├── 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 |
13 |
14 | | pyros2097 |
15 | | Gdx Developer |
16 | |
17 | "Awesomeness we can has"
18 | |
19 |
20 | |
21 |
22 |
23 |
24 |
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 [](https://github.com/pyros2097/rust-embed/actions?query=workflow%3ATest) [](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 |
--------------------------------------------------------------------------------