├── impl ├── license ├── readme.md ├── Cargo.toml └── src │ └── lib.rs ├── utils ├── license ├── readme.md ├── Cargo.toml └── src │ └── lib.rs ├── examples ├── public │ ├── symlinks │ │ └── main.js │ ├── images │ │ ├── doc.txt │ │ ├── llama.png │ │ └── flower.jpg │ ├── main.js │ ├── main.css │ └── index.html ├── axum-spa │ ├── assets │ │ ├── js │ │ │ └── script.js │ │ └── index.html │ ├── README.md │ └── main.rs ├── basic.rs ├── actix.rs ├── rocket.rs ├── warp.rs ├── salvo.rs ├── poem.rs └── axum.rs ├── .gitignore ├── renovate.json ├── rustfmt.toml ├── .pants-ignore ├── tests ├── metadata_only.rs ├── prefix.rs ├── custom_crate_path.rs ├── path_traversal_attack.rs ├── interpolated_path.rs ├── mime_guess.rs ├── metadata.rs ├── lib.rs └── include_exclude.rs ├── license ├── .github └── workflows │ └── test.yml ├── src └── lib.rs ├── Cargo.toml ├── readme.md └── changelog.md /impl/license: -------------------------------------------------------------------------------- 1 | ../license -------------------------------------------------------------------------------- /utils/license: -------------------------------------------------------------------------------- 1 | ../license -------------------------------------------------------------------------------- /examples/public/symlinks/main.js: -------------------------------------------------------------------------------- 1 | ../main.js -------------------------------------------------------------------------------- /examples/public/images/doc.txt: -------------------------------------------------------------------------------- 1 | Testing 1 2 3 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | 4 | .vscode 5 | .idea 6 | -------------------------------------------------------------------------------- /examples/public/main.js: -------------------------------------------------------------------------------- 1 | console.log("Awesomeness we can has") 2 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /impl/readme.md: -------------------------------------------------------------------------------- 1 | # Rust Embed Implementation 2 | 3 | The implementation of the rust-embed macro lies here. 4 | -------------------------------------------------------------------------------- /examples/public/images/llama.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wulf/rust-embed/master/examples/public/images/llama.png -------------------------------------------------------------------------------- /utils/readme.md: -------------------------------------------------------------------------------- 1 | # Rust Embed Utilities 2 | 3 | The utilities used by rust-embed and rust-embed-impl lie here. 4 | -------------------------------------------------------------------------------- /examples/public/images/flower.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wulf/rust-embed/master/examples/public/images/flower.jpg -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | merge_derives = true 2 | fn_params_layout = "Compressed" 3 | max_width = 160 4 | tab_spaces = 2 5 | reorder_imports = true 6 | -------------------------------------------------------------------------------- /.pants-ignore: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": [{ "id": "sonatype-2021-4646", "reason": "We are handling it this case. Sonatype doesn't recognize that." }] 3 | } 4 | -------------------------------------------------------------------------------- /examples/axum-spa/assets/js/script.js: -------------------------------------------------------------------------------- 1 | var elem = document.createElement("div"); 2 | elem.innerHTML = "I'm the JS script!
" + new Date(); 3 | document.body.appendChild(elem); 4 | -------------------------------------------------------------------------------- /examples/axum-spa/README.md: -------------------------------------------------------------------------------- 1 | A small example for hosting single page application (SPA) files with [axum](https://github.com/tokio-rs/axum) and [rust-embed](https://github.com/pyrossh/rust-embed). -------------------------------------------------------------------------------- /examples/public/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | color:#000000; 3 | font-family:sans-serif; 4 | font-size: small; 5 | font-style: italic; 6 | font-weight: bold; 7 | } 8 | 9 | table.hh { 10 | border:1px solid gray; 11 | } 12 | 13 | 14 | bor { border: 1px;} 15 | -------------------------------------------------------------------------------- /examples/basic.rs: -------------------------------------------------------------------------------- 1 | use rust_embed::Embed; 2 | 3 | #[derive(Embed)] 4 | #[folder = "examples/public/"] 5 | struct Asset; 6 | 7 | fn main() { 8 | let index_html = Asset::get("index.html").unwrap(); 9 | println!("{:?}", std::str::from_utf8(index_html.data.as_ref())); 10 | } 11 | -------------------------------------------------------------------------------- /tests/metadata_only.rs: -------------------------------------------------------------------------------- 1 | use rust_embed::{Embed, EmbeddedFile}; 2 | 3 | #[derive(Embed)] 4 | #[folder = "examples/public/"] 5 | #[metadata_only = true] 6 | struct Asset; 7 | 8 | #[test] 9 | fn file_is_empty() { 10 | let index_file: EmbeddedFile = Asset::get("index.html").expect("index.html exists"); 11 | assert_eq!(index_file.data.len(), 0); 12 | } 13 | -------------------------------------------------------------------------------- /examples/axum-spa/assets/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Single Page Application 8 | 9 | 10 | 11 | I'm the body! 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/prefix.rs: -------------------------------------------------------------------------------- 1 | use rust_embed::Embed; 2 | 3 | #[derive(Embed)] 4 | #[folder = "examples/public/"] 5 | #[prefix = "prefix/"] 6 | struct Asset; 7 | 8 | #[test] 9 | fn get_with_prefix() { 10 | assert!(Asset::get("prefix/index.html").is_some()); 11 | } 12 | 13 | #[test] 14 | fn get_without_prefix() { 15 | assert!(Asset::get("index.html").is_none()); 16 | } 17 | 18 | #[test] 19 | fn iter_values_have_prefix() { 20 | for file in Asset::iter() { 21 | assert!(file.starts_with("prefix/")); 22 | assert!(Asset::get(file.as_ref()).is_some()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 21 | 22 | 23 | 24 |
13 | 14 | 15 | 16 | 19 |
pyros2097
Gdx Developer
17 | "Awesomeness we can has" 18 |
20 |
25 |
26 | 27 | 28 | -------------------------------------------------------------------------------- /tests/custom_crate_path.rs: -------------------------------------------------------------------------------- 1 | /// This test checks that the `crate_path` attribute can be used 2 | /// to specify a custom path to the `rust_embed` crate. 3 | 4 | mod custom { 5 | pub mod path { 6 | pub use rust_embed; 7 | } 8 | } 9 | 10 | // We introduce a 'rust_embed' module here to break compilation in case 11 | // the `rust_embed` crate is not loaded correctly. 12 | // 13 | // To test this, try commenting out the attribute which specifies the 14 | // the custom crate path -- you should find that the test fails to compile. 15 | mod rust_embed {} 16 | 17 | #[derive(custom::path::rust_embed::RustEmbed)] 18 | #[crate_path = "custom::path::rust_embed"] 19 | #[folder = "examples/public/"] 20 | struct Asset; 21 | -------------------------------------------------------------------------------- /utils/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rust-embed-utils" 3 | version = "8.4.0" 4 | description = "Utilities for rust-embed" 5 | readme = "readme.md" 6 | documentation = "https://docs.rs/rust-embed" 7 | repository = "https://github.com/pyros2097/rust-embed" 8 | license = "MIT" 9 | keywords = ["http", "rocket", "static", "web", "server"] 10 | categories = ["web-programming::http-server"] 11 | authors = ["pyros2097 "] 12 | edition = "2018" 13 | 14 | [dependencies] 15 | walkdir = "2.3.1" 16 | sha2 = "0.10.5" 17 | mime_guess = { version = "2.0.4", optional = true } 18 | 19 | [dependencies.globset] 20 | version = "0.4.8" 21 | optional = true 22 | 23 | [features] 24 | debug-embed = [] 25 | mime-guess = ["mime_guess"] 26 | include-exclude = ["globset"] 27 | -------------------------------------------------------------------------------- /tests/path_traversal_attack.rs: -------------------------------------------------------------------------------- 1 | use rust_embed::Embed; 2 | 3 | #[derive(Embed)] 4 | #[folder = "examples/public/"] 5 | struct Assets; 6 | 7 | /// Prevent attempts to access files outside of the embedded folder. 8 | /// This is mainly a concern when running in debug mode, since that loads from 9 | /// the file system at runtime. 10 | #[test] 11 | fn path_traversal_attack_fails() { 12 | assert!(Assets::get("../basic.rs").is_none()); 13 | } 14 | 15 | #[derive(Embed)] 16 | #[folder = "examples/axum-spa/"] 17 | struct AxumAssets; 18 | 19 | // TODO: 20 | /// Prevent attempts to access symlinks outside of the embedded folder. 21 | /// This is mainly a concern when running in debug mode, since that loads from 22 | /// the file system at runtime. 23 | #[test] 24 | #[ignore = "see https://github.com/pyrossh/rust-embed/pull/235"] 25 | fn path_traversal_attack_symlink_fails() { 26 | assert!(Assets::get("../public/symlinks/main.js").is_none()); 27 | } 28 | -------------------------------------------------------------------------------- /examples/actix.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{web, App, HttpResponse, HttpServer, Responder}; 2 | use mime_guess::from_path; 3 | use rust_embed::Embed; 4 | 5 | #[derive(Embed)] 6 | #[folder = "examples/public/"] 7 | struct Asset; 8 | 9 | fn handle_embedded_file(path: &str) -> HttpResponse { 10 | match Asset::get(path) { 11 | Some(content) => HttpResponse::Ok() 12 | .content_type(from_path(path).first_or_octet_stream().as_ref()) 13 | .body(content.data.into_owned()), 14 | None => HttpResponse::NotFound().body("404 Not Found"), 15 | } 16 | } 17 | 18 | #[actix_web::get("/")] 19 | async fn index() -> impl Responder { 20 | handle_embedded_file("index.html") 21 | } 22 | 23 | #[actix_web::get("/dist/{_:.*}")] 24 | async fn dist(path: web::Path) -> impl Responder { 25 | handle_embedded_file(path.as_str()) 26 | } 27 | 28 | #[actix_web::main] 29 | async fn main() -> std::io::Result<()> { 30 | HttpServer::new(|| App::new().service(index).service(dist)).bind("127.0.0.1:8000")?.run().await 31 | } 32 | -------------------------------------------------------------------------------- /examples/rocket.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate rocket; 3 | 4 | use rocket::http::ContentType; 5 | use rocket::response::content::RawHtml; 6 | use rust_embed::Embed; 7 | 8 | use std::borrow::Cow; 9 | use std::ffi::OsStr; 10 | use std::path::PathBuf; 11 | 12 | #[derive(Embed)] 13 | #[folder = "examples/public/"] 14 | struct Asset; 15 | 16 | #[get("/")] 17 | fn index() -> Option>> { 18 | let asset = Asset::get("index.html")?; 19 | Some(RawHtml(asset.data)) 20 | } 21 | 22 | #[get("/dist/")] 23 | fn dist(file: PathBuf) -> Option<(ContentType, Cow<'static, [u8]>)> { 24 | let filename = file.display().to_string(); 25 | let asset = Asset::get(&filename)?; 26 | let content_type = file 27 | .extension() 28 | .and_then(OsStr::to_str) 29 | .and_then(ContentType::from_extension) 30 | .unwrap_or(ContentType::Bytes); 31 | 32 | Some((content_type, asset.data)) 33 | } 34 | 35 | #[rocket::launch] 36 | fn rocket() -> _ { 37 | rocket::build().mount("/", routes![index, dist]) 38 | } 39 | -------------------------------------------------------------------------------- /tests/interpolated_path.rs: -------------------------------------------------------------------------------- 1 | use rust_embed::Embed; 2 | 3 | /// Test doc comment 4 | #[derive(Embed)] 5 | #[folder = "$CARGO_MANIFEST_DIR/examples/public/"] 6 | struct Asset; 7 | 8 | #[test] 9 | fn get_works() { 10 | assert!(Asset::get("index.html").is_some(), "index.html should exist"); 11 | assert!(Asset::get("gg.html").is_none(), "gg.html should not exist"); 12 | assert!(Asset::get("images/llama.png").is_some(), "llama.png should exist"); 13 | } 14 | 15 | #[test] 16 | fn iter_works() { 17 | let mut num_files = 0; 18 | for file in Asset::iter() { 19 | assert!(Asset::get(file.as_ref()).is_some()); 20 | num_files += 1; 21 | } 22 | assert_eq!(num_files, 7); 23 | } 24 | 25 | #[test] 26 | fn trait_works_generic() { 27 | trait_works_generic_helper::(); 28 | } 29 | fn trait_works_generic_helper() { 30 | let mut num_files = 0; 31 | for file in E::iter() { 32 | assert!(E::get(file.as_ref()).is_some()); 33 | num_files += 1; 34 | } 35 | assert_eq!(num_files, 7); 36 | assert!(E::get("gg.html").is_none(), "gg.html should not exist"); 37 | } 38 | -------------------------------------------------------------------------------- /examples/warp.rs: -------------------------------------------------------------------------------- 1 | use rust_embed::Embed; 2 | use warp::{http::header::HeaderValue, path::Tail, reply::Response, Filter, Rejection, Reply}; 3 | 4 | #[derive(Embed)] 5 | #[folder = "examples/public/"] 6 | struct Asset; 7 | 8 | #[tokio::main] 9 | async fn main() { 10 | let index_html = warp::path::end().and_then(serve_index); 11 | let dist = warp::path("dist").and(warp::path::tail()).and_then(serve); 12 | 13 | let routes = index_html.or(dist); 14 | warp::serve(routes).run(([127, 0, 0, 1], 8080)).await; 15 | } 16 | 17 | async fn serve_index() -> Result { 18 | serve_impl("index.html") 19 | } 20 | 21 | async fn serve(path: Tail) -> Result { 22 | serve_impl(path.as_str()) 23 | } 24 | 25 | fn serve_impl(path: &str) -> Result { 26 | let asset = Asset::get(path).ok_or_else(warp::reject::not_found)?; 27 | let mime = mime_guess::from_path(path).first_or_octet_stream(); 28 | 29 | let mut res = Response::new(asset.data.into()); 30 | res.headers_mut().insert("content-type", HeaderValue::from_str(mime.as_ref()).unwrap()); 31 | Ok(res) 32 | } 33 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 pyros2097 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /impl/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rust-embed-impl" 3 | version = "8.4.0" 4 | description = "Rust Custom Derive Macro which loads files into the rust binary at compile time during release and loads the file from the fs during dev" 5 | readme = "readme.md" 6 | documentation = "https://docs.rs/rust-embed" 7 | repository = "https://github.com/pyros2097/rust-embed" 8 | license = "MIT" 9 | keywords = ["http", "rocket", "static", "web", "server"] 10 | categories = ["web-programming::http-server"] 11 | authors = ["pyros2097 "] 12 | edition = "2018" 13 | 14 | [lib] 15 | proc-macro = true 16 | 17 | [dependencies] 18 | rust-embed-utils = { version = "8.4.0", path = "../utils"} 19 | 20 | syn = { version = "2", default-features = false, features = ["derive", "parsing", "proc-macro", "printing"] } 21 | quote = "1" 22 | proc-macro2 = "1" 23 | walkdir = "2.3.1" 24 | 25 | [dependencies.shellexpand] 26 | version = "3" 27 | optional = true 28 | 29 | [features] 30 | debug-embed = [] 31 | interpolate-folder-path = ["shellexpand"] 32 | compression = [] 33 | mime-guess = ["rust-embed-utils/mime-guess"] 34 | include-exclude = ["rust-embed-utils/include-exclude"] 35 | -------------------------------------------------------------------------------- /tests/mime_guess.rs: -------------------------------------------------------------------------------- 1 | use rust_embed::{Embed, EmbeddedFile}; 2 | 3 | #[derive(Embed)] 4 | #[folder = "examples/public/"] 5 | struct Asset; 6 | 7 | #[test] 8 | fn html_mime_is_correct() { 9 | let html_file: EmbeddedFile = Asset::get("index.html").expect("index.html exists"); 10 | assert_eq!(html_file.metadata.mimetype(), "text/html"); 11 | } 12 | 13 | #[test] 14 | fn css_mime_is_correct() { 15 | let css_file: EmbeddedFile = Asset::get("main.css").expect("main.css exists"); 16 | assert_eq!(css_file.metadata.mimetype(), "text/css"); 17 | } 18 | 19 | #[test] 20 | fn js_mime_is_correct() { 21 | let js_file: EmbeddedFile = Asset::get("main.js").expect("main.js exists"); 22 | assert_eq!(js_file.metadata.mimetype(), "application/javascript"); 23 | } 24 | 25 | #[test] 26 | fn jpg_mime_is_correct() { 27 | let jpg_file: EmbeddedFile = Asset::get("images/flower.jpg").expect("flower.jpg exists"); 28 | assert_eq!(jpg_file.metadata.mimetype(), "image/jpeg"); 29 | } 30 | 31 | #[test] 32 | fn png_mime_is_correct() { 33 | let png_file: EmbeddedFile = Asset::get("images/llama.png").expect("llama.png exists"); 34 | assert_eq!(png_file.metadata.mimetype(), "image/png"); 35 | } 36 | -------------------------------------------------------------------------------- /tests/metadata.rs: -------------------------------------------------------------------------------- 1 | use rust_embed::{Embed, EmbeddedFile}; 2 | use sha2::Digest; 3 | use std::{fs, time::SystemTime}; 4 | 5 | #[derive(Embed)] 6 | #[folder = "examples/public/"] 7 | struct Asset; 8 | 9 | #[test] 10 | fn hash_is_accurate() { 11 | let index_file: EmbeddedFile = Asset::get("index.html").expect("index.html exists"); 12 | let mut hasher = sha2::Sha256::new(); 13 | hasher.update(index_file.data); 14 | let expected_hash: [u8; 32] = hasher.finalize().into(); 15 | 16 | assert_eq!(index_file.metadata.sha256_hash(), expected_hash); 17 | } 18 | 19 | #[test] 20 | fn last_modified_is_accurate() { 21 | let index_file: EmbeddedFile = Asset::get("index.html").expect("index.html exists"); 22 | 23 | let metadata = fs::metadata(format!("{}/examples/public/index.html", env!("CARGO_MANIFEST_DIR"))).unwrap(); 24 | let expected_datetime_utc = metadata.modified().unwrap().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs(); 25 | 26 | assert_eq!(index_file.metadata.last_modified(), Some(expected_datetime_utc)); 27 | } 28 | 29 | #[test] 30 | fn create_is_accurate() { 31 | let index_file: EmbeddedFile = Asset::get("index.html").expect("index.html exists"); 32 | 33 | let metadata = fs::metadata(format!("{}/examples/public/index.html", env!("CARGO_MANIFEST_DIR"))).unwrap(); 34 | let expected_datetime_utc = metadata.created().unwrap().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs(); 35 | 36 | assert_eq!(index_file.metadata.created(), Some(expected_datetime_utc)); 37 | } 38 | -------------------------------------------------------------------------------- /examples/axum-spa/main.rs: -------------------------------------------------------------------------------- 1 | use axum::{ 2 | http::{header, StatusCode, Uri}, 3 | response::{Html, IntoResponse, Response}, 4 | routing::Router, 5 | }; 6 | use rust_embed::Embed; 7 | use std::net::SocketAddr; 8 | 9 | static INDEX_HTML: &str = "index.html"; 10 | 11 | #[derive(Embed)] 12 | #[folder = "examples/axum-spa/assets/"] 13 | struct Assets; 14 | 15 | #[tokio::main] 16 | async fn main() { 17 | let app = Router::new().fallback(static_handler); 18 | 19 | let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); 20 | println!("listening on {}", addr); 21 | axum::Server::bind(&addr).serve(app.into_make_service()).await.unwrap(); 22 | } 23 | 24 | async fn static_handler(uri: Uri) -> impl IntoResponse { 25 | let path = uri.path().trim_start_matches('/'); 26 | 27 | if path.is_empty() || path == INDEX_HTML { 28 | return index_html().await; 29 | } 30 | 31 | match Assets::get(path) { 32 | Some(content) => { 33 | let mime = mime_guess::from_path(path).first_or_octet_stream(); 34 | 35 | ([(header::CONTENT_TYPE, mime.as_ref())], content.data).into_response() 36 | } 37 | None => { 38 | if path.contains('.') { 39 | return not_found().await; 40 | } 41 | 42 | index_html().await 43 | } 44 | } 45 | } 46 | 47 | async fn index_html() -> Response { 48 | match Assets::get(INDEX_HTML) { 49 | Some(content) => Html(content.data).into_response(), 50 | None => not_found().await, 51 | } 52 | } 53 | 54 | async fn not_found() -> Response { 55 | (StatusCode::NOT_FOUND, "404").into_response() 56 | } 57 | -------------------------------------------------------------------------------- /examples/salvo.rs: -------------------------------------------------------------------------------- 1 | use salvo::http::{header, StatusCode}; 2 | use salvo::prelude::*; 3 | 4 | #[tokio::main] 5 | async fn main() -> Result<(), std::io::Error> { 6 | let router = Router::new() 7 | .push(Router::with_path("dist/<**>").get(static_embed)) 8 | .push(Router::with_path("/<**>").get(static_embed)); 9 | 10 | let listener = TcpListener::bind("127.0.0.1:3000"); 11 | Server::new(listener).serve(router).await; 12 | Ok(()) 13 | } 14 | 15 | #[derive(rust_embed::Embed)] 16 | #[folder = "examples/public/"] 17 | struct Asset; 18 | 19 | #[fn_handler] 20 | async fn static_embed(req: &mut Request, res: &mut Response) { 21 | let mut path: String = req.get_param("**").unwrap_or_default(); 22 | if path.is_empty() { 23 | path = "index.html".into(); 24 | } 25 | 26 | match Asset::get(&path) { 27 | Some(content) => { 28 | let hash = hex::encode(content.metadata.sha256_hash()); 29 | // if etag is matched, return 304 30 | if req 31 | .headers() 32 | .get(header::IF_NONE_MATCH) 33 | .map(|etag| etag.to_str().unwrap_or("000000").eq(&hash)) 34 | .unwrap_or(false) 35 | { 36 | res.set_status_code(StatusCode::NOT_MODIFIED); 37 | return; 38 | } 39 | 40 | // otherwise, return 200 with etag hash 41 | let body: Vec = content.data.into(); 42 | let mime = mime_guess::from_path(path).first_or_octet_stream(); 43 | res.headers_mut().insert(header::ETAG, hash.parse().unwrap()); 44 | res.render_binary(mime.as_ref().parse().unwrap(), &body); 45 | } 46 | None => res.set_status_code(StatusCode::NOT_FOUND), 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/lib.rs: -------------------------------------------------------------------------------- 1 | use rust_embed::{Embed, RustEmbed}; 2 | 3 | /// Test doc comment 4 | #[derive(Embed)] 5 | #[folder = "examples/public/"] 6 | struct Asset; 7 | 8 | #[derive(RustEmbed)] 9 | #[folder = "examples/public/"] 10 | struct AssetOld; 11 | 12 | #[test] 13 | fn get_works() { 14 | assert!(Asset::get("index.html").is_some(), "index.html should exist"); 15 | assert!(Asset::get("gg.html").is_none(), "gg.html should not exist"); 16 | assert!(Asset::get("images/llama.png").is_some(), "llama.png should exist"); 17 | } 18 | 19 | // Todo remove this test and rename RustEmbed trait to Embed on a new major release 20 | #[test] 21 | fn get_old_name_works() { 22 | assert!(AssetOld::get("index.html").is_some(), "index.html should exist"); 23 | assert!(AssetOld::get("gg.html").is_none(), "gg.html should not exist"); 24 | assert!(AssetOld::get("images/llama.png").is_some(), "llama.png should exist"); 25 | } 26 | 27 | /// Using Windows-style path separators (`\`) is acceptable 28 | #[test] 29 | fn get_windows_style() { 30 | assert!( 31 | Asset::get("images\\llama.png").is_some(), 32 | "llama.png should be accessible via \"images\\lama.png\"" 33 | ); 34 | } 35 | 36 | #[test] 37 | fn iter_works() { 38 | let mut num_files = 0; 39 | for file in Asset::iter() { 40 | assert!(Asset::get(file.as_ref()).is_some()); 41 | num_files += 1; 42 | } 43 | assert_eq!(num_files, 7); 44 | } 45 | 46 | #[test] 47 | fn trait_works_generic() { 48 | trait_works_generic_helper::(); 49 | } 50 | fn trait_works_generic_helper() { 51 | let mut num_files = 0; 52 | for file in E::iter() { 53 | assert!(E::get(file.as_ref()).is_some()); 54 | num_files += 1; 55 | } 56 | assert_eq!(num_files, 7); 57 | assert!(E::get("gg.html").is_none(), "gg.html should not exist"); 58 | } 59 | -------------------------------------------------------------------------------- /examples/poem.rs: -------------------------------------------------------------------------------- 1 | use poem::{ 2 | async_trait, 3 | http::{header, Method, StatusCode}, 4 | listener::TcpListener, 5 | Endpoint, Request, Response, Result, Route, Server, 6 | }; 7 | #[tokio::main] 8 | async fn main() -> Result<(), std::io::Error> { 9 | let app = Route::new().at("/", StaticEmbed).at("/index.html", StaticEmbed).nest("/dist", StaticEmbed); 10 | 11 | let listener = TcpListener::bind("127.0.0.1:3000"); 12 | let server = Server::new(listener); 13 | server.run(app).await?; 14 | Ok(()) 15 | } 16 | 17 | #[derive(rust_embed::Embed)] 18 | #[folder = "examples/public/"] 19 | struct Asset; 20 | pub(crate) struct StaticEmbed; 21 | 22 | #[async_trait] 23 | impl Endpoint for StaticEmbed { 24 | type Output = Response; 25 | 26 | async fn call(&self, req: Request) -> Result { 27 | if req.method() != Method::GET { 28 | return Ok(StatusCode::METHOD_NOT_ALLOWED.into()); 29 | } 30 | 31 | let mut path = req.uri().path().trim_start_matches('/').trim_end_matches('/').to_string(); 32 | if path.starts_with("dist/") { 33 | path = path.replace("dist/", ""); 34 | } else if path.is_empty() { 35 | path = "index.html".to_string(); 36 | } 37 | let path = path.as_ref(); 38 | 39 | match Asset::get(path) { 40 | Some(content) => { 41 | let hash = hex::encode(content.metadata.sha256_hash()); 42 | // if etag is matched, return 304 43 | if req 44 | .headers() 45 | .get(header::IF_NONE_MATCH) 46 | .map(|etag| etag.to_str().unwrap_or("000000").eq(&hash)) 47 | .unwrap_or(false) 48 | { 49 | return Ok(StatusCode::NOT_MODIFIED.into()); 50 | } 51 | 52 | // otherwise, return 200 with etag hash 53 | let body: Vec = content.data.into(); 54 | let mime = mime_guess::from_path(path).first_or_octet_stream(); 55 | Ok( 56 | Response::builder() 57 | .header(header::CONTENT_TYPE, mime.as_ref()) 58 | .header(header::ETAG, hash) 59 | .body(body), 60 | ) 61 | } 62 | None => Ok(Response::builder().status(StatusCode::NOT_FOUND).finish()), 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: [push, pull_request] 3 | jobs: 4 | format: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions-rs/toolchain@v1 8 | with: 9 | toolchain: stable 10 | - uses: actions/checkout@master 11 | - run: rustup component add rustfmt 12 | - run: cargo fmt --all -- --check 13 | test: 14 | runs-on: ${{ matrix.os }} 15 | continue-on-error: ${{ matrix.experimental }} 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | os: [ubuntu-latest, windows-latest, macOS-latest] 20 | rust: [stable] 21 | experimental: [false] 22 | include: 23 | - rust: nightly 24 | os: ubuntu-latest 25 | experimental: true 26 | steps: 27 | - uses: actions-rs/toolchain@v1 28 | with: 29 | toolchain: ${{ matrix.rust }} 30 | - uses: actions/checkout@master 31 | - name: Run tests 32 | run: | 33 | cargo test --test lib 34 | cargo test --test lib --features "debug-embed" 35 | cargo test --test lib --features "compression" --release 36 | cargo test --test mime_guess --features "mime-guess" 37 | cargo test --test mime_guess --features "mime-guess" --release 38 | cargo test --test interpolated_path --features "interpolate-folder-path" 39 | cargo test --test interpolated_path --features "interpolate-folder-path" --release 40 | cargo test --test custom_crate_path 41 | cargo test --test custom_crate_path --release 42 | cargo build --example basic 43 | cargo build --example rocket --features rocket 44 | cargo build --example actix --features actix 45 | cargo build --example axum --features axum-ex 46 | cargo build --example warp --features warp-ex 47 | cargo test --test lib --release 48 | cargo build --example basic --release 49 | cargo build --example rocket --features rocket --release 50 | cargo build --example actix --features actix --release 51 | cargo build --example axum --features axum-ex --release 52 | cargo build --example warp --features warp-ex --release 53 | -------------------------------------------------------------------------------- /examples/axum.rs: -------------------------------------------------------------------------------- 1 | use axum::{ 2 | http::{header, StatusCode, Uri}, 3 | response::{Html, IntoResponse, Response}, 4 | routing::{get, Router}, 5 | }; 6 | use rust_embed::Embed; 7 | use std::net::SocketAddr; 8 | 9 | #[tokio::main] 10 | async fn main() { 11 | // Define our app routes, including a fallback option for anything not matched. 12 | let app = Router::new() 13 | .route("/", get(index_handler)) 14 | .route("/index.html", get(index_handler)) 15 | .route("/dist/*file", get(static_handler)) 16 | .fallback_service(get(not_found)); 17 | 18 | // Start listening on the given address. 19 | let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); 20 | println!("listening on {}", addr); 21 | let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); 22 | axum::serve(listener, app.into_make_service()).await.unwrap(); 23 | } 24 | 25 | // We use static route matchers ("/" and "/index.html") to serve our home 26 | // page. 27 | async fn index_handler() -> impl IntoResponse { 28 | static_handler("/index.html".parse::().unwrap()).await 29 | } 30 | 31 | // We use a wildcard matcher ("/dist/*file") to match against everything 32 | // within our defined assets directory. This is the directory on our Asset 33 | // struct below, where folder = "examples/public/". 34 | async fn static_handler(uri: Uri) -> impl IntoResponse { 35 | let mut path = uri.path().trim_start_matches('/').to_string(); 36 | 37 | if path.starts_with("dist/") { 38 | path = path.replace("dist/", ""); 39 | } 40 | 41 | StaticFile(path) 42 | } 43 | 44 | // Finally, we use a fallback route for anything that didn't match. 45 | async fn not_found() -> Html<&'static str> { 46 | Html("

404

Not Found

") 47 | } 48 | 49 | #[derive(Embed)] 50 | #[folder = "examples/public/"] 51 | struct Asset; 52 | 53 | pub struct StaticFile(pub T); 54 | 55 | impl IntoResponse for StaticFile 56 | where 57 | T: Into, 58 | { 59 | fn into_response(self) -> Response { 60 | let path = self.0.into(); 61 | 62 | match Asset::get(path.as_str()) { 63 | Some(content) => { 64 | let mime = mime_guess::from_path(path).first_or_octet_stream(); 65 | ([(header::CONTENT_TYPE, mime.as_ref())], content.data).into_response() 66 | } 67 | None => (StatusCode::NOT_FOUND, "404 Not Found").into_response(), 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/include_exclude.rs: -------------------------------------------------------------------------------- 1 | use rust_embed::Embed; 2 | 3 | #[derive(Embed)] 4 | #[folder = "examples/public/"] 5 | struct AllAssets; 6 | 7 | #[test] 8 | fn get_works() { 9 | assert!(AllAssets::get("index.html").is_some(), "index.html should exist"); 10 | assert!(AllAssets::get("gg.html").is_none(), "gg.html should not exist"); 11 | assert!(AllAssets::get("images/llama.png").is_some(), "llama.png should exist"); 12 | assert!(AllAssets::get("symlinks/main.js").is_some(), "main.js should exist"); 13 | assert_eq!(AllAssets::iter().count(), 7); 14 | } 15 | 16 | #[derive(Embed)] 17 | #[folder = "examples/public/"] 18 | #[include = "*.html"] 19 | #[include = "images/*"] 20 | struct IncludeSomeAssets; 21 | 22 | #[test] 23 | fn including_some_assets_works() { 24 | assert!(IncludeSomeAssets::get("index.html").is_some(), "index.html should exist"); 25 | assert!(IncludeSomeAssets::get("main.js").is_none(), "main.js should not exist"); 26 | assert!(IncludeSomeAssets::get("images/llama.png").is_some(), "llama.png should exist"); 27 | assert_eq!(IncludeSomeAssets::iter().count(), 4); 28 | } 29 | 30 | #[derive(Embed)] 31 | #[folder = "examples/public/"] 32 | #[exclude = "*.html"] 33 | #[exclude = "images/*"] 34 | struct ExcludeSomeAssets; 35 | 36 | #[test] 37 | fn excluding_some_assets_works() { 38 | assert!(ExcludeSomeAssets::get("index.html").is_none(), "index.html should not exist"); 39 | assert!(ExcludeSomeAssets::get("main.js").is_some(), "main.js should exist"); 40 | assert!(ExcludeSomeAssets::get("symlinks/main.js").is_some(), "main.js symlink should exist"); 41 | assert!(ExcludeSomeAssets::get("images/llama.png").is_none(), "llama.png should not exist"); 42 | assert_eq!(ExcludeSomeAssets::iter().count(), 3); 43 | } 44 | 45 | #[derive(Embed)] 46 | #[folder = "examples/public/"] 47 | #[include = "images/*"] 48 | #[exclude = "*.txt"] 49 | struct ExcludePriorityAssets; 50 | 51 | #[test] 52 | fn exclude_has_higher_priority() { 53 | assert!(ExcludePriorityAssets::get("images/doc.txt").is_none(), "doc.txt should not exist"); 54 | assert!(ExcludePriorityAssets::get("images/llama.png").is_some(), "llama.png should exist"); 55 | assert_eq!(ExcludePriorityAssets::iter().count(), 2); 56 | } 57 | 58 | #[derive(Embed)] 59 | #[folder = "examples/public/symlinks"] 60 | #[include = "main.js"] 61 | struct IncludeSymlink; 62 | 63 | #[test] 64 | fn include_symlink() { 65 | assert_eq!(IncludeSymlink::iter().count(), 1); 66 | assert_eq!(IncludeSymlink::iter().next(), Some(std::borrow::Cow::Borrowed("main.js"))); 67 | assert!(IncludeSymlink::get("main.js").is_some()) 68 | } 69 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![forbid(unsafe_code)] 2 | #[cfg(feature = "compression")] 3 | #[cfg_attr(feature = "compression", doc(hidden))] 4 | pub use include_flate::flate; 5 | 6 | #[allow(unused_imports)] 7 | #[macro_use] 8 | extern crate rust_embed_impl; 9 | pub use rust_embed_impl::*; 10 | 11 | pub use rust_embed_utils::{EmbeddedFile, Metadata}; 12 | 13 | #[doc(hidden)] 14 | pub extern crate rust_embed_utils as utils; 15 | 16 | /// A directory of binary assets. 17 | /// 18 | /// The files in the specified folder will be embedded into the executable in 19 | /// release builds. Debug builds will read the data from the file system at 20 | /// runtime. 21 | /// 22 | /// This trait is meant to be derived like so: 23 | /// ``` 24 | /// use rust_embed::Embed; 25 | /// 26 | /// #[derive(Embed)] 27 | /// #[folder = "examples/public/"] 28 | /// struct Asset; 29 | /// 30 | /// fn main() {} 31 | /// ``` 32 | pub trait RustEmbed { 33 | /// Get an embedded file and its metadata. 34 | /// 35 | /// If the feature `debug-embed` is enabled or the binary was compiled in 36 | /// release mode, the file information is embedded in the binary and the file 37 | /// data is returned as a `Cow::Borrowed(&'static [u8])`. 38 | /// 39 | /// Otherwise, the information is read from the file system on each call and 40 | /// the file data is returned as a `Cow::Owned(Vec)`. 41 | fn get(file_path: &str) -> Option; 42 | 43 | /// Iterates over the file paths in the folder. 44 | /// 45 | /// If the feature `debug-embed` is enabled or the binary is compiled in 46 | /// release mode, a static array containing the list of relative file paths 47 | /// is used. 48 | /// 49 | /// Otherwise, the files are listed from the file system on each call. 50 | fn iter() -> Filenames; 51 | } 52 | 53 | pub use RustEmbed as Embed; 54 | 55 | /// An iterator over filenames. 56 | /// 57 | /// This enum exists for optimization purposes, to avoid boxing the iterator in 58 | /// some cases. Do not try and match on it, as different variants will exist 59 | /// depending on the compilation context. 60 | pub enum Filenames { 61 | /// Release builds use a named iterator type, which can be stack-allocated. 62 | #[cfg(any(not(debug_assertions), feature = "debug-embed"))] 63 | Embedded(std::slice::Iter<'static, &'static str>), 64 | 65 | /// The debug iterator type is currently unnameable and still needs to be 66 | /// boxed. 67 | #[cfg(all(debug_assertions, not(feature = "debug-embed")))] 68 | Dynamic(Box>>), 69 | } 70 | 71 | impl Iterator for Filenames { 72 | type Item = std::borrow::Cow<'static, str>; 73 | fn next(&mut self) -> Option { 74 | match self { 75 | #[cfg(any(not(debug_assertions), feature = "debug-embed"))] 76 | Filenames::Embedded(names) => names.next().map(|x| std::borrow::Cow::from(*x)), 77 | 78 | #[cfg(all(debug_assertions, not(feature = "debug-embed")))] 79 | Filenames::Dynamic(boxed) => boxed.next(), 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rust-embed" 3 | version = "8.4.0" 4 | description = "Rust Custom Derive Macro which loads files into the rust binary at compile time during release and loads the file from the fs during dev" 5 | readme = "readme.md" 6 | documentation = "https://docs.rs/rust-embed" 7 | repository = "https://github.com/pyros2097/rust-embed" 8 | license = "MIT" 9 | keywords = ["http", "rocket", "static", "web", "server"] 10 | categories = ["web-programming", "filesystem"] 11 | authors = ["pyros2097 "] 12 | edition = "2018" 13 | rust-version = "1.70.0" 14 | 15 | [[example]] 16 | name = "warp" 17 | path = "examples/warp.rs" 18 | required-features = ["warp-ex"] 19 | 20 | [[example]] 21 | name = "actix" 22 | path = "examples/actix.rs" 23 | required-features = ["actix"] 24 | 25 | [[example]] 26 | name = "rocket" 27 | path = "examples/rocket.rs" 28 | required-features = ["rocket"] 29 | 30 | [[example]] 31 | name = "axum" 32 | path = "examples/axum.rs" 33 | required-features = ["axum-ex"] 34 | 35 | [[example]] 36 | name = "axum-spa" 37 | path = "examples/axum-spa/main.rs" 38 | required-features = ["axum-ex"] 39 | 40 | [[example]] 41 | name = "poem" 42 | path = "examples/poem.rs" 43 | required-features = ["poem-ex"] 44 | 45 | [[example]] 46 | name = "salvo" 47 | path = "examples/salvo.rs" 48 | required-features = ["salvo-ex"] 49 | 50 | [[test]] 51 | name = "interpolated_path" 52 | path = "tests/interpolated_path.rs" 53 | required-features = ["interpolate-folder-path"] 54 | 55 | [[test]] 56 | name = "include_exclude" 57 | path = "tests/include_exclude.rs" 58 | required-features = ["include-exclude"] 59 | 60 | [[test]] 61 | name = "mime_guess" 62 | path = "tests/mime_guess.rs" 63 | required-features = ["mime-guess"] 64 | 65 | [dependencies] 66 | walkdir = "2.3.2" 67 | rust-embed-impl = { version = "8.4.0", path = "impl"} 68 | rust-embed-utils = { version = "8.4.0", path = "utils"} 69 | 70 | include-flate = { version = "0.2", optional = true, features = ["stable"] } 71 | actix-web = { version = "4", optional = true } 72 | mime_guess = { version = "2", optional = true } 73 | hex = { version = "0.4.3", optional = true } 74 | tokio = { version = "1.0", optional = true, features = ["macros", "rt-multi-thread"] } 75 | warp = { version = "0.3", default-features = false, optional = true } 76 | rocket = { version = "0.5.0-rc.2", default-features = false, optional = true } 77 | axum = { version = "0.7", default-features = false, features = ["http1", "tokio"], optional = true } 78 | poem = { version = "1.3.30", default-features = false, features = ["server"], optional = true } 79 | salvo = { version = "0.16", default-features = false, optional = true } 80 | 81 | [dev-dependencies] 82 | sha2 = "0.10" 83 | 84 | [features] 85 | debug-embed = ["rust-embed-impl/debug-embed", "rust-embed-utils/debug-embed"] 86 | interpolate-folder-path = ["rust-embed-impl/interpolate-folder-path"] 87 | compression = ["rust-embed-impl/compression", "include-flate"] 88 | mime-guess = ["rust-embed-impl/mime-guess", "rust-embed-utils/mime-guess"] 89 | include-exclude = ["rust-embed-impl/include-exclude", "rust-embed-utils/include-exclude"] 90 | actix = ["actix-web", "mime_guess"] 91 | warp-ex = ["warp", "tokio", "mime_guess"] 92 | axum-ex = ["axum", "tokio", "mime_guess"] 93 | poem-ex = ["poem", "tokio", "mime_guess", "hex"] 94 | salvo-ex = ["salvo", "tokio", "mime_guess", "hex"] 95 | 96 | 97 | [badges] 98 | appveyor = { repository = "pyros2097/rust-embed" } 99 | travis-ci = { repository = "pyros2097/rust-embed" } 100 | is-it-maintained-issue-resolution = { repository = "pyros2097/rust-embed" } 101 | is-it-maintained-open-issues = { repository = "pyros2097/rust-embed" } 102 | maintenance = { status = "passively-maintained" } 103 | 104 | [workspace] 105 | members = ["impl", "utils"] 106 | -------------------------------------------------------------------------------- /utils/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![forbid(unsafe_code)] 2 | 3 | use sha2::Digest; 4 | use std::borrow::Cow; 5 | use std::path::Path; 6 | use std::time::SystemTime; 7 | use std::{fs, io}; 8 | 9 | #[cfg_attr(all(debug_assertions, not(feature = "debug-embed")), allow(unused))] 10 | pub struct FileEntry { 11 | pub rel_path: String, 12 | pub full_canonical_path: String, 13 | } 14 | 15 | #[cfg_attr(all(debug_assertions, not(feature = "debug-embed")), allow(unused))] 16 | pub fn get_files(folder_path: String, matcher: PathMatcher) -> impl Iterator { 17 | walkdir::WalkDir::new(&folder_path) 18 | .follow_links(true) 19 | .sort_by_file_name() 20 | .into_iter() 21 | .filter_map(|e| e.ok()) 22 | .filter(|e| e.file_type().is_file()) 23 | .filter_map(move |e| { 24 | let rel_path = path_to_str(e.path().strip_prefix(&folder_path).unwrap()); 25 | let full_canonical_path = path_to_str(std::fs::canonicalize(e.path()).expect("Could not get canonical path")); 26 | 27 | let rel_path = if std::path::MAIN_SEPARATOR == '\\' { 28 | rel_path.replace('\\', "/") 29 | } else { 30 | rel_path 31 | }; 32 | if matcher.is_path_included(&rel_path) { 33 | Some(FileEntry { rel_path, full_canonical_path }) 34 | } else { 35 | None 36 | } 37 | }) 38 | } 39 | 40 | /// A file embedded into the binary 41 | #[derive(Clone)] 42 | pub struct EmbeddedFile { 43 | pub data: Cow<'static, [u8]>, 44 | pub metadata: Metadata, 45 | } 46 | 47 | /// Metadata about an embedded file 48 | #[derive(Clone)] 49 | pub struct Metadata { 50 | hash: [u8; 32], 51 | last_modified: Option, 52 | created: Option, 53 | #[cfg(feature = "mime-guess")] 54 | mimetype: Cow<'static, str>, 55 | } 56 | 57 | impl Metadata { 58 | #[doc(hidden)] 59 | pub const fn __rust_embed_new( 60 | hash: [u8; 32], last_modified: Option, created: Option, #[cfg(feature = "mime-guess")] mimetype: &'static str, 61 | ) -> Self { 62 | Self { 63 | hash, 64 | last_modified, 65 | created, 66 | #[cfg(feature = "mime-guess")] 67 | mimetype: Cow::Borrowed(mimetype), 68 | } 69 | } 70 | 71 | /// The SHA256 hash of the file 72 | pub fn sha256_hash(&self) -> [u8; 32] { 73 | self.hash 74 | } 75 | 76 | /// The last modified date in seconds since the UNIX epoch. If the underlying 77 | /// platform/file-system does not support this, None is returned. 78 | pub fn last_modified(&self) -> Option { 79 | self.last_modified 80 | } 81 | 82 | /// The created data in seconds since the UNIX epoch. If the underlying 83 | /// platform/file-system does not support this, None is returned. 84 | pub fn created(&self) -> Option { 85 | self.created 86 | } 87 | 88 | /// The mime type of the file 89 | #[cfg(feature = "mime-guess")] 90 | pub fn mimetype(&self) -> &str { 91 | &self.mimetype 92 | } 93 | } 94 | 95 | pub fn read_file_from_fs(file_path: &Path) -> io::Result { 96 | let data = fs::read(file_path)?; 97 | let data = Cow::from(data); 98 | 99 | let mut hasher = sha2::Sha256::new(); 100 | hasher.update(&data); 101 | let hash: [u8; 32] = hasher.finalize().into(); 102 | 103 | let source_date_epoch = match std::env::var("SOURCE_DATE_EPOCH") { 104 | Ok(value) => value.parse::().ok(), 105 | Err(_) => None, 106 | }; 107 | 108 | let metadata = fs::metadata(file_path)?; 109 | let last_modified = metadata 110 | .modified() 111 | .ok() 112 | .and_then(|modified| modified.duration_since(SystemTime::UNIX_EPOCH).ok()) 113 | .map(|secs| secs.as_secs()); 114 | 115 | let created = metadata 116 | .created() 117 | .ok() 118 | .and_then(|created| created.duration_since(SystemTime::UNIX_EPOCH).ok()) 119 | .map(|secs| secs.as_secs()); 120 | 121 | #[cfg(feature = "mime-guess")] 122 | let mimetype = mime_guess::from_path(file_path).first_or_octet_stream().to_string(); 123 | 124 | Ok(EmbeddedFile { 125 | data, 126 | metadata: Metadata { 127 | hash, 128 | last_modified: source_date_epoch.or(last_modified), 129 | created: source_date_epoch.or(created), 130 | #[cfg(feature = "mime-guess")] 131 | mimetype: mimetype.into(), 132 | }, 133 | }) 134 | } 135 | 136 | fn path_to_str>(p: P) -> String { 137 | p.as_ref().to_str().expect("Path does not have a string representation").to_owned() 138 | } 139 | 140 | #[derive(Clone)] 141 | pub struct PathMatcher { 142 | #[cfg(feature = "include-exclude")] 143 | include_matcher: globset::GlobSet, 144 | #[cfg(feature = "include-exclude")] 145 | exclude_matcher: globset::GlobSet, 146 | } 147 | 148 | #[cfg(feature = "include-exclude")] 149 | impl PathMatcher { 150 | pub fn new(includes: &[&str], excludes: &[&str]) -> Self { 151 | let mut include_matcher = globset::GlobSetBuilder::new(); 152 | for include in includes { 153 | include_matcher.add(globset::Glob::new(include).unwrap_or_else(|_| panic!("invalid include pattern '{}'", include))); 154 | } 155 | let include_matcher = include_matcher 156 | .build() 157 | .unwrap_or_else(|_| panic!("Could not compile included patterns matcher")); 158 | 159 | let mut exclude_matcher = globset::GlobSetBuilder::new(); 160 | for exclude in excludes { 161 | exclude_matcher.add(globset::Glob::new(exclude).unwrap_or_else(|_| panic!("invalid exclude pattern '{}'", exclude))); 162 | } 163 | let exclude_matcher = exclude_matcher 164 | .build() 165 | .unwrap_or_else(|_| panic!("Could not compile excluded patterns matcher")); 166 | 167 | Self { 168 | include_matcher, 169 | exclude_matcher, 170 | } 171 | } 172 | pub fn is_path_included(&self, path: &str) -> bool { 173 | !self.exclude_matcher.is_match(path) && (self.include_matcher.is_empty() || self.include_matcher.is_match(path)) 174 | } 175 | } 176 | 177 | #[cfg(not(feature = "include-exclude"))] 178 | impl PathMatcher { 179 | pub fn new(_includes: &[&str], _excludes: &[&str]) -> Self { 180 | Self {} 181 | } 182 | pub fn is_path_included(&self, _path: &str) -> bool { 183 | true 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ## Rust Embed [![Build Status](https://github.com/pyros2097/rust-embed/workflows/Test/badge.svg)](https://github.com/pyros2097/rust-embed/actions?query=workflow%3ATest) [![crates.io](https://img.shields.io/crates/v/rust-embed.svg)](https://crates.io/crates/rust-embed) 2 | 3 | Rust Custom Derive Macro which loads files into the rust binary at compile time during release and loads the file from the fs during dev. 4 | 5 | You can use this to embed your css, js and images into a single executable which can be deployed to your servers. Also it makes it easy to build a very small docker image for you to deploy. 6 | 7 | ## Installation 8 | 9 | ```toml 10 | [dependencies] 11 | rust-embed="8.4.0" 12 | ``` 13 | 14 | ## Documentation 15 | 16 | You need to add the custom derive macro RustEmbed to your struct with an attribute `folder` which is the path to your static folder. 17 | 18 | The path resolution works as follows: 19 | 20 | - In `debug` and when `debug-embed` feature is not enabled, the folder path is resolved relative to where the binary is run from. 21 | - In `release` or when `debug-embed` feature is enabled, the folder path is resolved relative to where `Cargo.toml` is. 22 | 23 | ```rust 24 | #[derive(Embed)] 25 | #[folder = "examples/public/"] 26 | struct Asset; 27 | ``` 28 | 29 | The macro will generate the following code: 30 | 31 | ```rust 32 | impl Asset { 33 | pub fn get(file_path: &str) -> Option { 34 | ... 35 | } 36 | 37 | pub fn iter() -> impl Iterator> { 38 | ... 39 | } 40 | } 41 | impl RustEmbed for Asset { 42 | fn get(file_path: &str) -> Option { 43 | ... 44 | } 45 | fn iter() -> impl Iterator> { 46 | ... 47 | } 48 | } 49 | 50 | // Where EmbeddedFile contains these fields, 51 | pub struct EmbeddedFile { 52 | pub data: Cow<'static, [u8]>, 53 | pub metadata: Metadata, 54 | } 55 | pub struct Metadata { 56 | hash: [u8; 32], 57 | last_modified: Option, 58 | created: Option, 59 | } 60 | ``` 61 | 62 | ### `get(file_path: &str) -> Option` 63 | 64 | Given a relative path from the assets folder returns the `EmbeddedFile` if found. 65 | 66 | If the feature `debug-embed` is enabled or the binary compiled in release mode the bytes have been embeded in the binary and a `Option` is returned. 67 | 68 | Otherwise the bytes are read from the file system on each call and a `Option` is returned. 69 | 70 | ### `iter()` 71 | 72 | Iterates the files in this assets folder. 73 | 74 | If the feature `debug-embed` is enabled or the binary compiled in release mode a static array to the list of relative paths to the files is returned. 75 | 76 | Otherwise the files are listed from the file system on each call. 77 | 78 | ## Attributes 79 | ### `prefix` 80 | 81 | You can add `#[prefix = "my_prefix/"]` to the `RustEmbed` struct to add a prefix 82 | to all of the file paths. This prefix will be required on `get` calls, and will 83 | be included in the file paths returned by `iter`. 84 | 85 | ### `metadata_only` 86 | 87 | You can add `#[metadata_only = true]` to the `RustEmbed` struct to exclude file contents from the 88 | binary. Only file paths and metadata will be embedded. 89 | 90 | ## Features 91 | 92 | ### `debug-embed` 93 | 94 | Always embed the files in the binary, even in debug mode. 95 | 96 | ### `interpolate-folder-path` 97 | 98 | Allow environment variables to be used in the `folder` path. Example: 99 | 100 | ```rust 101 | #[derive(Embed)] 102 | #[folder = "$CARGO_MANIFEST_DIR/foo"] 103 | struct Asset; 104 | ``` 105 | 106 | This will pull the `foo` directory relative to your `Cargo.toml` file. 107 | 108 | ### `compression` 109 | 110 | Compress each file when embedding into the binary. Compression is done via [`include-flate`]. 111 | 112 | ### `include-exclude` 113 | Filter files to be embedded with multiple `#[include = "*.txt"]` and `#[exclude = "*.jpg"]` attributes. 114 | Matching is done on relative file paths, via [`globset`]. 115 | `exclude` attributes have higher priority than `include` attributes. 116 | Example: 117 | 118 | ```rust 119 | use rust_embed::Embed; 120 | 121 | #[derive(Embed)] 122 | #[folder = "examples/public/"] 123 | #[include = "*.html"] 124 | #[include = "images/*"] 125 | #[exclude = "*.txt"] 126 | struct Asset; 127 | ``` 128 | 129 | ## Usage 130 | 131 | ```rust 132 | use rust_embed::Embed; 133 | 134 | #[derive(Embed)] 135 | #[folder = "examples/public/"] 136 | #[prefix = "prefix/"] 137 | struct Asset; 138 | 139 | fn main() { 140 | let index_html = Asset::get("prefix/index.html").unwrap(); 141 | println!("{:?}", std::str::from_utf8(index_html.data.as_ref())); 142 | 143 | for file in Asset::iter() { 144 | println!("{}", file.as_ref()); 145 | } 146 | } 147 | ``` 148 | 149 | ## Integrations 150 | 151 | 1. [Poem](https://github.com/poem-web/poem) for poem framework under feature flag "embed" 152 | 2. [warp_embed](https://docs.rs/warp-embed/latest/warp_embed/) for warp framework 153 | 154 | ## Examples 155 | 156 | To run the example in dev mode where it reads from the fs, 157 | 158 | `cargo run --example basic` 159 | 160 | To run the example in release mode where it reads from binary, 161 | 162 | `cargo run --example basic --release` 163 | 164 | Note: To run the [actix-web](https://github.com/actix/actix-web) example: 165 | 166 | `cargo run --example actix --features actix` 167 | 168 | Note: To run the [rocket](https://github.com/SergioBenitez/Rocket) example: 169 | 170 | `cargo run --example rocket --features rocket` 171 | 172 | Note: To run the [warp](https://github.com/seanmonstar/warp) example: 173 | 174 | `cargo run --example warp --features warp-ex` 175 | 176 | Note: To run the [axum](https://github.com/tokio-rs/axum) example: 177 | 178 | `cargo run --example axum --features axum-ex` 179 | 180 | Note: To run the [poem](https://github.com/poem-web/poem) example: 181 | 182 | `cargo run --example poem --features poem-ex` 183 | 184 | Note: To run the [salvo](https://github.com/salvo-rs/salvo) example: 185 | 186 | `cargo run --example salvo --features salvo-ex` 187 | 188 | ## Testing 189 | 190 | debug: `cargo test --test lib` 191 | 192 | release: `cargo test --test lib --release` 193 | 194 | Go Rusketeers! 195 | The power is yours! 196 | 197 | [`include-flate`]: https://crates.io/crates/include-flate 198 | [`globset`]: https://crates.io/crates/globset 199 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 7 | 8 | Thanks to [Mark Drobnak](https://github.com/AzureMarker) for the changelog. 9 | 10 | ## [8.4.0] - 2024-05-11 11 | 12 | - Re-export RustEmbed as Embed [#245](https://github.com/pyrossh/rust-embed/pull/245/files). Thanks to [pyrossh](https://github.com/pyrossh) 13 | - Do not build glob matchers repeatedly when include-exclude feature is enabled [#244](https://github.com/pyrossh/rust-embed/pull/244/files). Thanks to [osiewicz](https://github.com/osiewicz) 14 | - Add `metadata_only` attribute [#241](https://github.com/pyrossh/rust-embed/pull/241/files). Thanks to [ddfisher](https://github.com/ddfisher) 15 | - Replace `expect` with a safer alternative that returns `None` instead [#240](https://github.com/pyrossh/rust-embed/pull/240/files). Thanks to [costinsin](https://github.com/costinsin) 16 | - Eliminate unnecessary `to_path` call [#239](https://github.com/pyrossh/rust-embed/pull/239/files). Thanks to [smoelius](https://github.com/smoelius) 17 | 18 | ## [8.3.0] - 2024-02-26 19 | 20 | - Fix symbolic links in debug builds [#235](https://github.com/pyrossh/rust-embed/pull/235/files). Thanks to [Buckram123](https://github.com/Buckram123) 21 | 22 | ## [8.2.0] - 2023-12-29 23 | 24 | - Fix naming collisions in macros [#230](https://github.com/pyrossh/rust-embed/pull/230/files). Thanks to [hwittenborn](https://github.com/hwittenborn) 25 | 26 | ## [8.1.0] - 2023-12-08 27 | 28 | - Add created to file metadata. [#225](https://github.com/pyrossh/rust-embed/pull/225/files). Thanks to [ngalaiko](https://github.com/ngalaiko) 29 | 30 | ## [8.0.0] - 2023-08-23 31 | 32 | - Store file contents statically and use binary search for lookup. [#217](https://github.com/pyrossh/rust-embed/pull/217/files). Thanks to [osiewicz](https://github.com/osiewicz) 33 | 34 | ## [6.8.1] - 2023-06-30 35 | 36 | - Fix failing compilation under compression feature [#182](https://github.com/pyrossh/rust-embed/issues/182). Thanks to [osiewicz](https://github.com/osiewicz) 37 | 38 | ## [6.8.0] - 2023-06-30 39 | 40 | - Update `include-flate` to v0.2 [#182](https://github.com/pyrossh/rust-embed/issues/182) 41 | 42 | ## [6.7.0] - 2023-06-09 43 | 44 | - Update `syn` to v2.0 [#211](https://github.com/pyrossh/rust-embed/issues/211) 45 | 46 | ## [6.6.1] - 2023-03-25 47 | 48 | - Fix mime-guess feature not working properly [#209](https://github.com/pyrossh/rust-embed/issues/209) 49 | 50 | ## [6.6.0] - 2023-03-05 51 | 52 | - sort_by_file_name() requires walkdir v2.3.2 [#206](https://github.com/pyrossh/rust-embed/issues/206) 53 | - Add `mime-guess` feature to statically store mimetype [#192](https://github.com/pyrossh/rust-embed/issues/192) 54 | 55 | ## [6.4.2] - 2022-10-20 56 | 57 | - Fail the proc macro if include/exclude are used without the feature [#187](https://github.com/pyrossh/rust-embed/issues/187) 58 | 59 | ## [6.4.1] - 2022-09-13 60 | 61 | - Update sha2 dependency version in utils crate [#186](https://github.com/pyrossh/rust-embed/issues/186) 62 | 63 | ## [6.4.0] - 2022-04-15 64 | 65 | - Order files by filename [#171](https://github.com/pyros2097/rust-embed/issues/171). Thanks to [apognu](https://github.com/apognu) 66 | 67 | ## [6.3.0] - 2021-11-28 68 | 69 | - Fixed a security issue in debug mode [#159](https://github.com/pyros2097/rust-embed/issues/159). Thanks to [5225225](https://github.com/5225225) 70 | 71 | ## [6.2.0] - 2021-09-01 72 | 73 | - Fixed `include-exclude` feature when using cargo v2 resolver 74 | 75 | ## [6.1.0] - 2021-08-31 76 | 77 | - Added `include-exclude` feature by [mbme](https://github.com/mbme) 78 | 79 | ## [6.0.1] - 2021-08-21 80 | 81 | - Added doc comments to macro generated functions 82 | 83 | ## [6.0.0] - 2021-08-01 84 | 85 | Idea came about from [Cody Casterline](https://github.com/NfNitLoop) 86 | 87 | - Breaking change the `Asset::get()` api has changed and now returns an `EmbeddedFile` which contains a `data` field which is the bytes of the file and 88 | a `metadata` field which has theses 2 properties associated to the file `hash` and `last_modified`; 89 | 90 | ## [5.9.0] - 2021-01-18 91 | 92 | ### Added 93 | 94 | - Added path prefix attribute 95 | 96 | ## [5.8.0] - 2021-01-06 97 | 98 | ### Fixed 99 | 100 | - Fixed compiling with latest version of syn 101 | 102 | ## [5.7.0] - 2020-12-08 103 | 104 | ### Fixed 105 | 106 | - Fix feature flag typo 107 | 108 | ## [5.6.0] - 2020-07-19 109 | 110 | ### Fixed 111 | 112 | - Fixed windows path error in release mode 113 | 114 | ### Changed 115 | 116 | - Using github actions for CI now 117 | 118 | ## [5.5.1] - 2020-03-19 119 | 120 | ### Fixed 121 | 122 | - Fixed warnings in latest nightly 123 | 124 | ## [5.5.0] - 2020-02-26 125 | 126 | ### Fixed 127 | 128 | - Fixed the `folder` directory being relative to the current directory. 129 | It is now relative to `Cargo.toml`. 130 | 131 | ## [5.4.0] - 2020-02-24 132 | 133 | ### Changed 134 | 135 | - using rust-2018 edition now 136 | - code cleanup 137 | - updated examples and crates 138 | 139 | ## [5.3.0] - 2020-02-15 140 | 141 | ### Added 142 | 143 | - `compression` feature for compressing embedded files 144 | 145 | ## [5.2.0] - 2019-12-05 146 | 147 | ## Changed 148 | 149 | - updated syn and quote crate to 1.x 150 | 151 | ## [5.1.0] - 2019-07-09 152 | 153 | ## Fixed 154 | 155 | - error when debug code tries to import the utils crate 156 | 157 | ## [5.0.1] - 2019-07-07 158 | 159 | ## Changed 160 | 161 | - derive is allowed only on unit structs now 162 | 163 | ## [5.0.0] - 2019-07-05 164 | 165 | ## Added 166 | 167 | - proper error message stating only unit structs are supported 168 | 169 | ## Fixed 170 | 171 | - windows latest build 172 | 173 | ## [4.5.0] - 2019-06-29 174 | 175 | ## Added 176 | 177 | - allow rust embed derive to take env variables in the folder path 178 | 179 | ## [4.4.0] - 2019-05-11 180 | 181 | ### Fixed 182 | 183 | - a panic when struct has doc comments 184 | 185 | ### Added 186 | 187 | - a warp example 188 | 189 | ## [4.3.0] - 2019-01-10 190 | 191 | ### Fixed 192 | 193 | - debug_embed feature was not working at all 194 | 195 | ### Added 196 | 197 | - a test run for debug_embed feature 198 | 199 | ## [4.2.0] - 2018-12-02 200 | 201 | ### Changed 202 | 203 | - return `Cow<'static, [u8]>` to preserve static lifetime 204 | 205 | ## [4.1.0] - 2018-10-24 206 | 207 | ### Added 208 | 209 | - `iter()` method to list files 210 | 211 | ## [4.0.0] - 2018-10-11 212 | 213 | ### Changed 214 | 215 | - avoid vector allocation by returning `impl AsRef<[u8]>` 216 | 217 | ## [3.0.2] - 2018-09-05 218 | 219 | ### Added 220 | 221 | - appveyor for testing on windows 222 | 223 | ### Fixed 224 | 225 | - handle paths in windows correctly 226 | 227 | ## [3.0.1] - 2018-07-24 228 | 229 | ### Added 230 | 231 | - panic if the folder cannot be found 232 | 233 | ## [3.0.0] - 2018-06-01 234 | 235 | ### Changed 236 | 237 | - The derive attribute style so we don't need `attr_literals` and it can be used in stable rust now. Thanks to [Mcat12](https://github.com/Mcat12). 238 | 239 | ```rust 240 | #[folder("assets/")] 241 | ``` 242 | 243 | to 244 | 245 | ```rust 246 | #[folder = "assets/"] 247 | ``` 248 | 249 | ### Removed 250 | 251 | - log dependecy as we are not using it anymore 252 | 253 | ## [2.0.0] - 2018-05-26 254 | 255 | ### Changed 256 | 257 | - Reimplemented the macro for release to use include_bytes for perf sake. Thanks to [lukad](https://github.com/lukad). 258 | 259 | ## [1.1.1] - 2018-03-19 260 | 261 | ### Changed 262 | 263 | - Fixed usage error message 264 | 265 | ## [1.1.0] - 2018-03-19 266 | 267 | ### Added 268 | 269 | - Release mode for custom derive 270 | 271 | ### Changed 272 | 273 | - Fixed tests in travis 274 | 275 | ## [1.0.0] - 2018-03-18 276 | 277 | ### Changed 278 | 279 | - Converted the rust-embed macro `embed!` into a Rust Custom Derive Macro `#[derive(RustEmbed)]` which implements get on the struct 280 | 281 | ```rust 282 | let asset = embed!("examples/public/") 283 | ``` 284 | 285 | to 286 | 287 | ```rust 288 | #[derive(RustEmbed)] 289 | #[folder = "examples/public/"] 290 | struct Asset; 291 | ``` 292 | 293 | ## [0.5.2] - 2018-03-16 294 | 295 | ### Added 296 | 297 | - rouille example 298 | 299 | ## [0.5.1] - 2018-03-16 300 | 301 | ### Removed 302 | 303 | - the plugin attribute from crate 304 | 305 | ## [0.5.0] - 2018-03-16 306 | 307 | ### Added 308 | 309 | - rocket example 310 | 311 | ### Changed 312 | 313 | - Converted the rust-embed executable into a macro `embed!` which now loads files at compile time during release and from the fs during dev. 314 | 315 | ## [0.4.0] - 2017-03-2 316 | 317 | ### Changed 318 | 319 | - `generate_assets` to public again 320 | 321 | ## [0.3.5] - 2017-03-2 322 | 323 | ### Added 324 | 325 | - rust-embed prefix to all logs 326 | 327 | ## [0.3.4] - 2017-03-2 328 | 329 | ### Changed 330 | 331 | - the lib to be plugin again 332 | 333 | ## [0.3.3] - 2017-03-2 334 | 335 | ### Changed 336 | 337 | - the lib to be proc-macro from plugin 338 | 339 | ## [0.3.2] - 2017-03-2 340 | 341 | ### Changed 342 | 343 | - lib name from `rust-embed` to `rust_embed` 344 | 345 | ## [0.3.1] - 2017-03-2 346 | 347 | ### Removed 348 | 349 | - hyper example 350 | 351 | ## [0.3.0] - 2017-02-26 352 | 353 | ### Added 354 | 355 | - rust-embed executable which generates rust code to embed resource files into your rust executable 356 | it creates a file like assets.rs that contains the code for your assets. 357 | 358 | ## [0.2.0] - 2017-03-16 359 | 360 | ### Added 361 | 362 | - rust-embed executable which generates rust code to embed resource files into your rust executable 363 | it creates a file like assets.rs that contains the code for your assets. 364 | -------------------------------------------------------------------------------- /impl/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![recursion_limit = "1024"] 2 | #![forbid(unsafe_code)] 3 | #[macro_use] 4 | extern crate quote; 5 | extern crate proc_macro; 6 | 7 | use proc_macro::TokenStream; 8 | use proc_macro2::TokenStream as TokenStream2; 9 | use rust_embed_utils::PathMatcher; 10 | use std::{ 11 | collections::BTreeMap, 12 | env, 13 | iter::FromIterator, 14 | path::{Path, PathBuf}, 15 | }; 16 | use syn::{parse_macro_input, Data, DeriveInput, Expr, ExprLit, Fields, Lit, Meta, MetaNameValue}; 17 | 18 | fn embedded( 19 | ident: &syn::Ident, relative_folder_path: Option<&str>, absolute_folder_path: String, prefix: Option<&str>, includes: &[String], excludes: &[String], 20 | metadata_only: bool, crate_path: &syn::Path, 21 | ) -> syn::Result { 22 | extern crate rust_embed_utils; 23 | 24 | let mut match_values = BTreeMap::new(); 25 | let mut list_values = Vec::::new(); 26 | 27 | let includes: Vec<&str> = includes.iter().map(AsRef::as_ref).collect(); 28 | let excludes: Vec<&str> = excludes.iter().map(AsRef::as_ref).collect(); 29 | let matcher = PathMatcher::new(&includes, &excludes); 30 | for rust_embed_utils::FileEntry { rel_path, full_canonical_path } in rust_embed_utils::get_files(absolute_folder_path.clone(), matcher) { 31 | match_values.insert( 32 | rel_path.clone(), 33 | embed_file(relative_folder_path, ident, &rel_path, &full_canonical_path, metadata_only, crate_path)?, 34 | ); 35 | 36 | list_values.push(if let Some(prefix) = prefix { 37 | format!("{}{}", prefix, rel_path) 38 | } else { 39 | rel_path 40 | }); 41 | } 42 | 43 | let array_len = list_values.len(); 44 | 45 | // If debug-embed is on, unconditionally include the code below. Otherwise, 46 | // make it conditional on cfg(not(debug_assertions)). 47 | let not_debug_attr = if cfg!(feature = "debug-embed") { 48 | quote! {} 49 | } else { 50 | quote! { #[cfg(not(debug_assertions))]} 51 | }; 52 | 53 | let handle_prefix = if let Some(prefix) = prefix { 54 | quote! { 55 | let file_path = file_path.strip_prefix(#prefix)?; 56 | } 57 | } else { 58 | TokenStream2::new() 59 | }; 60 | let match_values = match_values.into_iter().map(|(path, bytes)| { 61 | quote! { 62 | (#path, #bytes), 63 | } 64 | }); 65 | let value_type = if cfg!(feature = "compression") { 66 | quote! { fn() -> #crate_path::EmbeddedFile } 67 | } else { 68 | quote! { #crate_path::EmbeddedFile } 69 | }; 70 | let get_value = if cfg!(feature = "compression") { 71 | quote! {|idx| (ENTRIES[idx].1)()} 72 | } else { 73 | quote! {|idx| ENTRIES[idx].1.clone()} 74 | }; 75 | Ok(quote! { 76 | #not_debug_attr 77 | impl #ident { 78 | /// Get an embedded file and its metadata. 79 | pub fn get(file_path: &str) -> ::std::option::Option<#crate_path::EmbeddedFile> { 80 | #handle_prefix 81 | let key = file_path.replace("\\", "/"); 82 | const ENTRIES: &'static [(&'static str, #value_type)] = &[ 83 | #(#match_values)*]; 84 | let position = ENTRIES.binary_search_by_key(&key.as_str(), |entry| entry.0); 85 | position.ok().map(#get_value) 86 | 87 | } 88 | 89 | fn names() -> ::std::slice::Iter<'static, &'static str> { 90 | const ITEMS: [&str; #array_len] = [#(#list_values),*]; 91 | ITEMS.iter() 92 | } 93 | 94 | /// Iterates over the file paths in the folder. 95 | pub fn iter() -> impl ::std::iter::Iterator> { 96 | Self::names().map(|x| ::std::borrow::Cow::from(*x)) 97 | } 98 | } 99 | 100 | #not_debug_attr 101 | impl #crate_path::RustEmbed for #ident { 102 | fn get(file_path: &str) -> ::std::option::Option<#crate_path::EmbeddedFile> { 103 | #ident::get(file_path) 104 | } 105 | fn iter() -> #crate_path::Filenames { 106 | #crate_path::Filenames::Embedded(#ident::names()) 107 | } 108 | } 109 | }) 110 | } 111 | 112 | fn dynamic( 113 | ident: &syn::Ident, folder_path: String, prefix: Option<&str>, includes: &[String], excludes: &[String], metadata_only: bool, crate_path: &syn::Path, 114 | ) -> TokenStream2 { 115 | let (handle_prefix, map_iter) = if let ::std::option::Option::Some(prefix) = prefix { 116 | ( 117 | quote! { let file_path = file_path.strip_prefix(#prefix)?; }, 118 | quote! { ::std::borrow::Cow::Owned(format!("{}{}", #prefix, e.rel_path)) }, 119 | ) 120 | } else { 121 | (TokenStream2::new(), quote! { ::std::borrow::Cow::from(e.rel_path) }) 122 | }; 123 | 124 | let declare_includes = quote! { 125 | const INCLUDES: &[&str] = &[#(#includes),*]; 126 | }; 127 | 128 | let declare_excludes = quote! { 129 | const EXCLUDES: &[&str] = &[#(#excludes),*]; 130 | }; 131 | 132 | // In metadata_only mode, we still need to read file contents to generate the 133 | // file hash, but then we drop the file data. 134 | let strip_contents = metadata_only.then_some(quote! { 135 | .map(|mut file| { file.data = ::std::default::Default::default(); file }) 136 | }); 137 | 138 | let canonical_folder_path = Path::new(&folder_path).canonicalize().expect("folder path must resolve to an absolute path"); 139 | let canonical_folder_path = canonical_folder_path.to_str().expect("absolute folder path must be valid unicode"); 140 | 141 | quote! { 142 | #[cfg(debug_assertions)] 143 | impl #ident { 144 | 145 | 146 | fn matcher() -> #crate_path::utils::PathMatcher { 147 | #declare_includes 148 | #declare_excludes 149 | static PATH_MATCHER: ::std::sync::OnceLock<#crate_path::utils::PathMatcher> = ::std::sync::OnceLock::new(); 150 | PATH_MATCHER.get_or_init(|| #crate_path::utils::PathMatcher::new(INCLUDES, EXCLUDES)).clone() 151 | } 152 | /// Get an embedded file and its metadata. 153 | pub fn get(file_path: &str) -> ::std::option::Option<#crate_path::EmbeddedFile> { 154 | #handle_prefix 155 | 156 | let rel_file_path = file_path.replace("\\", "/"); 157 | let file_path = ::std::path::Path::new(#folder_path).join(&rel_file_path); 158 | 159 | // Make sure the path requested does not escape the folder path 160 | let canonical_file_path = file_path.canonicalize().ok()?; 161 | if !canonical_file_path.starts_with(#canonical_folder_path) { 162 | // Tried to request a path that is not in the embedded folder 163 | 164 | // TODO: Currently it allows "path_traversal_attack" for the symlink files 165 | // For it to be working properly we need to get absolute path first 166 | // and check that instead if it starts with `canonical_folder_path` 167 | // https://doc.rust-lang.org/std/path/fn.absolute.html (currently nightly) 168 | // Should be allowed only if it was a symlink 169 | let metadata = ::std::fs::symlink_metadata(&file_path).ok()?; 170 | if !metadata.is_symlink() { 171 | return ::std::option::Option::None; 172 | } 173 | } 174 | let path_matcher = Self::matcher(); 175 | if path_matcher.is_path_included(&rel_file_path) { 176 | #crate_path::utils::read_file_from_fs(&canonical_file_path).ok() #strip_contents 177 | } else { 178 | ::std::option::Option::None 179 | } 180 | } 181 | 182 | /// Iterates over the file paths in the folder. 183 | pub fn iter() -> impl ::std::iter::Iterator> { 184 | use ::std::path::Path; 185 | 186 | 187 | #crate_path::utils::get_files(::std::string::String::from(#folder_path), Self::matcher()) 188 | .map(|e| #map_iter) 189 | } 190 | } 191 | 192 | #[cfg(debug_assertions)] 193 | impl #crate_path::RustEmbed for #ident { 194 | fn get(file_path: &str) -> ::std::option::Option<#crate_path::EmbeddedFile> { 195 | #ident::get(file_path) 196 | } 197 | fn iter() -> #crate_path::Filenames { 198 | // the return type of iter() is unnamable, so we have to box it 199 | #crate_path::Filenames::Dynamic(::std::boxed::Box::new(#ident::iter())) 200 | } 201 | } 202 | } 203 | } 204 | 205 | fn generate_assets( 206 | ident: &syn::Ident, relative_folder_path: Option<&str>, absolute_folder_path: String, prefix: Option, includes: Vec, excludes: Vec, 207 | metadata_only: bool, crate_path: &syn::Path, 208 | ) -> syn::Result { 209 | let embedded_impl = embedded( 210 | ident, 211 | relative_folder_path, 212 | absolute_folder_path.clone(), 213 | prefix.as_deref(), 214 | &includes, 215 | &excludes, 216 | metadata_only, 217 | crate_path, 218 | ); 219 | if cfg!(feature = "debug-embed") { 220 | return embedded_impl; 221 | } 222 | let embedded_impl = embedded_impl?; 223 | let dynamic_impl = dynamic(ident, absolute_folder_path, prefix.as_deref(), &includes, &excludes, metadata_only, crate_path); 224 | 225 | Ok(quote! { 226 | #embedded_impl 227 | #dynamic_impl 228 | }) 229 | } 230 | 231 | fn embed_file( 232 | folder_path: Option<&str>, ident: &syn::Ident, rel_path: &str, full_canonical_path: &str, metadata_only: bool, crate_path: &syn::Path, 233 | ) -> syn::Result { 234 | let file = rust_embed_utils::read_file_from_fs(Path::new(full_canonical_path)).expect("File should be readable"); 235 | let hash = file.metadata.sha256_hash(); 236 | let last_modified = match file.metadata.last_modified() { 237 | Some(last_modified) => quote! { ::std::option::Option::Some(#last_modified) }, 238 | None => quote! { ::std::option::Option::None }, 239 | }; 240 | let created = match file.metadata.created() { 241 | Some(created) => quote! { ::std::option::Option::Some(#created) }, 242 | None => quote! { ::std::option::Option::None }, 243 | }; 244 | #[cfg(feature = "mime-guess")] 245 | let mimetype_tokens = { 246 | let mt = file.metadata.mimetype(); 247 | quote! { , #mt } 248 | }; 249 | #[cfg(not(feature = "mime-guess"))] 250 | let mimetype_tokens = TokenStream2::new(); 251 | 252 | let embedding_code = if metadata_only { 253 | quote! { 254 | const BYTES: &'static [u8] = &[]; 255 | } 256 | } else if cfg!(feature = "compression") { 257 | let folder_path = folder_path.ok_or(syn::Error::new(ident.span(), "`folder` must be provided under `compression` feature."))?; 258 | // Print some debugging information 259 | let full_relative_path = PathBuf::from_iter([folder_path, rel_path]); 260 | let full_relative_path = full_relative_path.to_string_lossy(); 261 | quote! { 262 | #crate_path::flate!(static BYTES: [u8] from #full_relative_path); 263 | } 264 | } else { 265 | quote! { 266 | const BYTES: &'static [u8] = include_bytes!(#full_canonical_path); 267 | } 268 | }; 269 | let closure_args = if cfg!(feature = "compression") { 270 | quote! { || } 271 | } else { 272 | quote! {} 273 | }; 274 | Ok(quote! { 275 | #closure_args { 276 | #embedding_code 277 | 278 | #crate_path::EmbeddedFile { 279 | data: ::std::borrow::Cow::Borrowed(&BYTES), 280 | metadata: #crate_path::Metadata::__rust_embed_new([#(#hash),*], #last_modified, #created #mimetype_tokens) 281 | } 282 | } 283 | }) 284 | } 285 | 286 | /// Find all pairs of the `name = "value"` attribute from the derive input 287 | fn find_attribute_values(ast: &syn::DeriveInput, attr_name: &str) -> Vec { 288 | ast 289 | .attrs 290 | .iter() 291 | .filter(|value| value.path().is_ident(attr_name)) 292 | .filter_map(|attr| match &attr.meta { 293 | Meta::NameValue(MetaNameValue { 294 | value: Expr::Lit(ExprLit { lit: Lit::Str(val), .. }), 295 | .. 296 | }) => Some(val.value()), 297 | _ => None, 298 | }) 299 | .collect() 300 | } 301 | 302 | fn find_bool_attribute(ast: &syn::DeriveInput, attr_name: &str) -> Option { 303 | ast 304 | .attrs 305 | .iter() 306 | .find(|value| value.path().is_ident(attr_name)) 307 | .and_then(|attr| match &attr.meta { 308 | Meta::NameValue(MetaNameValue { 309 | value: Expr::Lit(ExprLit { lit: Lit::Bool(val), .. }), 310 | .. 311 | }) => Some(val.value()), 312 | _ => None, 313 | }) 314 | } 315 | 316 | fn impl_rust_embed(ast: &syn::DeriveInput) -> syn::Result { 317 | match ast.data { 318 | Data::Struct(ref data) => match data.fields { 319 | Fields::Unit => {} 320 | _ => return Err(syn::Error::new_spanned(ast, "RustEmbed can only be derived for unit structs")), 321 | }, 322 | _ => return Err(syn::Error::new_spanned(ast, "RustEmbed can only be derived for unit structs")), 323 | }; 324 | 325 | let crate_path: syn::Path = find_attribute_values(ast, "crate_path") 326 | .last() 327 | .map(|v| syn::parse_str(&v).unwrap()) 328 | .unwrap_or_else(|| syn::parse_str("rust_embed").unwrap()); 329 | 330 | let mut folder_paths = find_attribute_values(ast, "folder"); 331 | if folder_paths.len() != 1 { 332 | return Err(syn::Error::new_spanned( 333 | ast, 334 | "#[derive(RustEmbed)] must contain one attribute like this #[folder = \"examples/public/\"]", 335 | )); 336 | } 337 | let folder_path = folder_paths.remove(0); 338 | 339 | let prefix = find_attribute_values(ast, "prefix").into_iter().next(); 340 | let includes = find_attribute_values(ast, "include"); 341 | let excludes = find_attribute_values(ast, "exclude"); 342 | let metadata_only = find_bool_attribute(ast, "metadata_only").unwrap_or(false); 343 | 344 | #[cfg(not(feature = "include-exclude"))] 345 | if !includes.is_empty() || !excludes.is_empty() { 346 | return Err(syn::Error::new_spanned( 347 | ast, 348 | "Please turn on the `include-exclude` feature to use the `include` and `exclude` attributes", 349 | )); 350 | } 351 | 352 | #[cfg(feature = "interpolate-folder-path")] 353 | let folder_path = shellexpand::full(&folder_path) 354 | .map_err(|v| syn::Error::new_spanned(ast, v.to_string()))? 355 | .to_string(); 356 | 357 | // Base relative paths on the Cargo.toml location 358 | let (relative_path, absolute_folder_path) = if Path::new(&folder_path).is_relative() { 359 | let absolute_path = Path::new(&env::var("CARGO_MANIFEST_DIR").unwrap()) 360 | .join(&folder_path) 361 | .to_str() 362 | .unwrap() 363 | .to_owned(); 364 | (Some(folder_path.clone()), absolute_path) 365 | } else { 366 | if cfg!(feature = "compression") { 367 | return Err(syn::Error::new_spanned(ast, "`folder` must be a relative path under `compression` feature.")); 368 | } 369 | (None, folder_path) 370 | }; 371 | 372 | if !Path::new(&absolute_folder_path).exists() { 373 | let mut message = format!( 374 | "#[derive(RustEmbed)] folder '{}' does not exist. cwd: '{}'", 375 | absolute_folder_path, 376 | std::env::current_dir().unwrap().to_str().unwrap() 377 | ); 378 | 379 | // Add a message about the interpolate-folder-path feature if the path may 380 | // include a variable 381 | if absolute_folder_path.contains('$') && cfg!(not(feature = "interpolate-folder-path")) { 382 | message += "\nA variable has been detected. RustEmbed can expand variables \ 383 | when the `interpolate-folder-path` feature is enabled."; 384 | } 385 | 386 | return Err(syn::Error::new_spanned(ast, message)); 387 | }; 388 | 389 | generate_assets( 390 | &ast.ident, 391 | relative_path.as_deref(), 392 | absolute_folder_path, 393 | prefix, 394 | includes, 395 | excludes, 396 | metadata_only, 397 | &crate_path, 398 | ) 399 | } 400 | 401 | #[proc_macro_derive(RustEmbed, attributes(folder, prefix, include, exclude, metadata_only, crate_path))] 402 | pub fn derive_input_object(input: TokenStream) -> TokenStream { 403 | let ast = parse_macro_input!(input as DeriveInput); 404 | match impl_rust_embed(&ast) { 405 | Ok(ok) => ok.into(), 406 | Err(e) => e.to_compile_error().into(), 407 | } 408 | } 409 | --------------------------------------------------------------------------------