rust-embed v8.11.0
git clone https://git.pyrossh.dev/rust-embed
rust macro which loads files into the rust binary at compile time during release and loads the file from the fs during dev.
c3aed87
— Hugues Morisset
2026-07-04T12:33:42+05:30
Add support to get files dat compressed
- Cargo.toml +8 -3
- examples/axum.rs +36 -32
- impl/src/lib.rs +113 -34
- src/lib.rs +9 -0
- tests/compression.rs +92 -0
- tests/custom_crate_path.rs +3 -2
- tests/path_traversal_attack.rs +1 -0
- utils/Cargo.toml +3 -1
- utils/src/lib.rs +18 -0
Cargo.toml
CHANGED
|
@@ -10,7 +10,7 @@ keywords = ["http", "rocket", "static", "web", "server"]
|
|
|
10
10
|
categories = ["web-programming", "filesystem"]
|
|
11
11
|
authors = ["pyrossh"]
|
|
12
12
|
edition = "2018"
|
|
13
|
-
rust-version = "1.
|
|
13
|
+
rust-version = "1.80.0"
|
|
14
14
|
|
|
15
15
|
[[example]]
|
|
16
16
|
name = "warp"
|
|
@@ -47,6 +47,11 @@ name = "salvo"
|
|
|
47
47
|
path = "examples/salvo.rs"
|
|
48
48
|
required-features = ["salvo-ex"]
|
|
49
49
|
|
|
50
|
+
[[test]]
|
|
51
|
+
name = "compression"
|
|
52
|
+
path = "tests/compression.rs"
|
|
53
|
+
required-features = ["compression"]
|
|
54
|
+
|
|
50
55
|
[[test]]
|
|
51
56
|
name = "interpolated_path"
|
|
52
57
|
path = "tests/interpolated_path.rs"
|
|
@@ -87,12 +92,12 @@ poem = { version = "1.3.30", default-features = false, features = [
|
|
|
87
92
|
salvo = { version = "0.16", default-features = false, optional = true }
|
|
88
93
|
|
|
89
94
|
[dev-dependencies]
|
|
90
|
-
sha2 = "0.
|
|
95
|
+
sha2 = "0.11"
|
|
91
96
|
|
|
92
97
|
[features]
|
|
93
98
|
debug-embed = ["rust-embed-impl/debug-embed", "rust-embed-utils/debug-embed"]
|
|
94
99
|
interpolate-folder-path = ["rust-embed-impl/interpolate-folder-path"]
|
|
95
|
-
compression = ["rust-embed-impl/compression", "include-flate"]
|
|
100
|
+
compression = ["rust-embed-impl/compression", "rust-embed-utils/compression", "include-flate"]
|
|
96
101
|
mime-guess = ["rust-embed-utils/mime-guess"]
|
|
97
102
|
include-exclude = [
|
|
98
103
|
"rust-embed-impl/include-exclude",
|
examples/axum.rs
CHANGED
|
@@ -1,65 +1,69 @@
|
|
|
1
1
|
use axum::{
|
|
2
2
|
extract::Path,
|
|
3
|
-
http::{
|
|
3
|
+
http::{HeaderMap, StatusCode},
|
|
4
4
|
response::{Html, IntoResponse, Response},
|
|
5
5
|
routing::{get, Router},
|
|
6
6
|
};
|
|
7
7
|
use rust_embed::Embed;
|
|
8
8
|
use std::net::SocketAddr;
|
|
9
9
|
|
|
10
|
+
#[derive(Embed)]
|
|
11
|
+
#[folder = "examples/public/"]
|
|
12
|
+
#[compression = "zstd"]
|
|
13
|
+
struct Asset;
|
|
14
|
+
|
|
10
15
|
#[tokio::main]
|
|
11
16
|
async fn main() {
|
|
12
|
-
// Define our app routes, including a fallback option for anything not matched.
|
|
13
17
|
let app = Router::new()
|
|
14
18
|
.route("/", get(index_handler))
|
|
15
19
|
.route("/index.html", get(index_handler))
|
|
16
20
|
.route("/dist/{*file}", get(static_handler))
|
|
17
21
|
.fallback_service(get(not_found));
|
|
18
22
|
|
|
19
|
-
// Start listening on the given address.
|
|
20
23
|
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
|
|
21
24
|
println!("listening on {}", addr);
|
|
22
25
|
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
|
23
26
|
axum::serve(listener, app.into_make_service()).await.unwrap();
|
|
24
27
|
}
|
|
25
28
|
|
|
26
|
-
// We use static route matchers ("/" and "/index.html") to serve our home
|
|
27
|
-
// page.
|
|
28
|
-
async fn index_handler() -> impl IntoResponse {
|
|
29
|
+
async fn index_handler(headers: HeaderMap) -> impl IntoResponse {
|
|
29
|
-
|
|
30
|
+
serve_asset("index.html", &headers)
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
// We use a wildcard matcher ("/dist/*file") to match against everything
|
|
33
|
-
// within our defined assets directory. This is the directory on our Asset
|
|
34
|
-
// struct below, where folder = "examples/public/".
|
|
35
|
-
async fn static_handler(Path(path): Path<String>) -> impl IntoResponse {
|
|
33
|
+
async fn static_handler(Path(path): Path<String>, headers: HeaderMap) -> impl IntoResponse {
|
|
36
|
-
|
|
34
|
+
serve_asset(&path, &headers)
|
|
37
35
|
}
|
|
38
36
|
|
|
39
|
-
// Finally, we use a fallback route for anything that didn't match.
|
|
40
37
|
async fn not_found() -> Html<&'static str> {
|
|
41
38
|
Html("<h1>404</h1><p>Not Found</p>")
|
|
42
39
|
}
|
|
43
40
|
|
|
41
|
+
#[cfg_attr(not(feature = "compression"), allow(unused_variables, reason = "headers for compression path only"))]
|
|
44
|
-
|
|
42
|
+
fn serve_asset(path: &str, headers: &HeaderMap) -> Response {
|
|
45
|
-
#[
|
|
43
|
+
#[cfg(feature = "compression")]
|
|
46
|
-
struct Asset;
|
|
47
|
-
|
|
48
|
-
pub struct StaticFile<T>(pub T);
|
|
49
|
-
|
|
50
|
-
impl<T> IntoResponse for StaticFile<T>
|
|
51
|
-
where
|
|
52
|
-
T: Into<String>,
|
|
53
|
-
{
|
|
54
|
-
|
|
44
|
+
if let Some(compressed) = Asset::compressed(path) {
|
|
55
|
-
let
|
|
45
|
+
let encoding = compressed.content_encoding();
|
|
56
|
-
|
|
57
|
-
|
|
46
|
+
let accept = headers.get(axum::http::header::ACCEPT_ENCODING).and_then(|v| v.to_str().ok()).unwrap_or("");
|
|
58
|
-
|
|
47
|
+
if accept.contains(encoding) {
|
|
48
|
+
return (
|
|
49
|
+
[
|
|
59
|
-
|
|
50
|
+
#[cfg(feature = "mime-guess")]
|
|
60
|
-
|
|
51
|
+
(axum::http::header::CONTENT_TYPE, compressed.metadata.mimetype()),
|
|
61
|
-
}
|
|
62
|
-
|
|
52
|
+
(axum::http::header::CONTENT_ENCODING, encoding),
|
|
53
|
+
],
|
|
54
|
+
compressed.data.compressed().to_vec(),
|
|
55
|
+
)
|
|
56
|
+
.into_response();
|
|
63
57
|
}
|
|
64
58
|
}
|
|
59
|
+
|
|
60
|
+
match Asset::get(path) {
|
|
61
|
+
Some(content) => (
|
|
62
|
+
#[cfg(feature = "mime-guess")]
|
|
63
|
+
[(axum::http::header::CONTENT_TYPE, content.metadata.mimetype())],
|
|
64
|
+
content.data,
|
|
65
|
+
)
|
|
66
|
+
.into_response(),
|
|
67
|
+
None => (StatusCode::NOT_FOUND, "404 Not Found").into_response(),
|
|
68
|
+
}
|
|
65
69
|
}
|
impl/src/lib.rs
CHANGED
|
@@ -16,9 +16,10 @@ use std::{
|
|
|
16
16
|
};
|
|
17
17
|
use syn::{parse_macro_input, Data, DeriveInput, Expr, ExprLit, Fields, Lit, Meta, MetaNameValue};
|
|
18
18
|
|
|
19
|
+
#[allow(clippy::too_many_arguments)]
|
|
19
20
|
fn embedded(
|
|
20
21
|
ident: &syn::Ident, relative_folder_path: Option<&str>, absolute_folder_path: Option<String>, prefix: Option<&str>, includes: &[String], excludes: &[String],
|
|
21
|
-
metadata_only: bool, crate_path: &syn::Path,
|
|
22
|
+
metadata_only: bool, crate_path: &syn::Path, compression: &str,
|
|
22
23
|
) -> syn::Result<TokenStream2> {
|
|
23
24
|
extern crate rust_embed_utils;
|
|
24
25
|
|
|
@@ -35,7 +36,7 @@ fn embedded(
|
|
|
35
36
|
for rust_embed_utils::FileEntry { rel_path, full_canonical_path } in entries {
|
|
36
37
|
match_values.insert(
|
|
37
38
|
rel_path.clone(),
|
|
38
|
-
embed_file(relative_folder_path, ident, &rel_path, &full_canonical_path, metadata_only, crate_path)?,
|
|
39
|
+
embed_file(relative_folder_path, ident, &rel_path, &full_canonical_path, metadata_only, crate_path, compression)?,
|
|
39
40
|
);
|
|
40
41
|
|
|
41
42
|
list_values.push(if let Some(prefix) = prefix {
|
|
@@ -67,30 +68,74 @@ fn embedded(
|
|
|
67
68
|
(#path, #bytes),
|
|
68
69
|
}
|
|
69
70
|
});
|
|
70
|
-
let
|
|
71
|
+
let use_compression = cfg!(feature = "compression") && !metadata_only;
|
|
72
|
+
let (entry_type, return_type) = if use_compression {
|
|
73
|
+
(
|
|
71
|
-
|
|
74
|
+
quote! { fn() -> #crate_path::EmbeddedCompressedFile },
|
|
75
|
+
quote! { #crate_path::EmbeddedCompressedFile },
|
|
76
|
+
)
|
|
72
77
|
} else {
|
|
73
|
-
quote! { #crate_path::EmbeddedFile }
|
|
78
|
+
(quote! { #crate_path::EmbeddedFile }, quote! { #crate_path::EmbeddedFile })
|
|
74
79
|
};
|
|
75
|
-
let get_value = if
|
|
80
|
+
let get_value = if use_compression {
|
|
76
81
|
quote! {|idx| (ENTRIES[idx].1)()}
|
|
77
82
|
} else {
|
|
78
83
|
quote! {|idx| ENTRIES[idx].1.clone()}
|
|
79
84
|
};
|
|
85
|
+
let impls = if cfg!(feature = "compression") {
|
|
86
|
+
if metadata_only {
|
|
87
|
+
quote! {
|
|
88
|
+
pub fn compressed(_file_path: &str) -> ::std::option::Option<#crate_path::EmbeddedCompressedFile> {
|
|
89
|
+
::std::option::Option::None
|
|
90
|
+
}
|
|
91
|
+
pub fn get(file_path: &str) -> ::std::option::Option<#crate_path::EmbeddedFile> {
|
|
92
|
+
#ident::__file(file_path)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
quote! {
|
|
97
|
+
pub fn compressed(file_path: &str) -> ::std::option::Option<#crate_path::EmbeddedCompressedFile> {
|
|
98
|
+
#ident::__file(file_path)
|
|
99
|
+
}
|
|
100
|
+
pub fn get(file_path: &str) -> ::std::option::Option<#crate_path::EmbeddedFile> {
|
|
101
|
+
#ident::__file(file_path).map(|f| #crate_path::EmbeddedFile {
|
|
102
|
+
data: f.data.decoded().into(),
|
|
103
|
+
metadata: f.metadata,
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
quote! {
|
|
110
|
+
/// Get an embedded file and its metadata.
|
|
111
|
+
pub fn get(file_path: &str) -> ::std::option::Option<#crate_path::EmbeddedFile> {
|
|
112
|
+
#ident::__file(file_path)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
let compressed_trait_impl = if cfg!(feature = "compression") {
|
|
117
|
+
quote! {
|
|
118
|
+
fn compressed(file_path: &str) -> ::std::option::Option<#crate_path::EmbeddedCompressedFile> {
|
|
119
|
+
#ident::compressed(file_path)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
TokenStream2::new()
|
|
124
|
+
};
|
|
80
125
|
Ok(quote! {
|
|
81
126
|
#not_debug_attr
|
|
82
127
|
impl #ident {
|
|
83
|
-
/// Get an embedded file and its metadata.
|
|
84
|
-
|
|
128
|
+
fn __file(file_path: &str) -> ::std::option::Option<#return_type> {
|
|
85
|
-
|
|
129
|
+
#handle_prefix
|
|
86
|
-
|
|
130
|
+
let key = file_path.replace("\\", "/");
|
|
87
|
-
|
|
131
|
+
static ENTRIES: &'static [(&'static str, #entry_type)] = &[
|
|
88
|
-
|
|
132
|
+
#(#match_values)*];
|
|
89
|
-
|
|
133
|
+
let position = ENTRIES.binary_search_by_key(&key.as_str(), |entry| entry.0);
|
|
90
|
-
|
|
134
|
+
position.ok().map(#get_value)
|
|
91
|
-
|
|
92
135
|
}
|
|
93
136
|
|
|
137
|
+
#impls
|
|
138
|
+
|
|
94
139
|
fn names() -> ::std::slice::Iter<'static, &'static str> {
|
|
95
140
|
const ITEMS: [&str; #array_len] = [#(#list_values),*];
|
|
96
141
|
ITEMS.iter()
|
|
@@ -104,6 +149,7 @@ fn embedded(
|
|
|
104
149
|
|
|
105
150
|
#not_debug_attr
|
|
106
151
|
impl #crate_path::RustEmbed for #ident {
|
|
152
|
+
#compressed_trait_impl
|
|
107
153
|
fn get(file_path: &str) -> ::std::option::Option<#crate_path::EmbeddedFile> {
|
|
108
154
|
#ident::get(file_path)
|
|
109
155
|
}
|
|
@@ -117,6 +163,15 @@ fn embedded(
|
|
|
117
163
|
fn dynamic(
|
|
118
164
|
ident: &syn::Ident, folder_path: Option<String>, prefix: Option<&str>, includes: &[String], excludes: &[String], metadata_only: bool, crate_path: &syn::Path,
|
|
119
165
|
) -> TokenStream2 {
|
|
166
|
+
let dynamic_compressed = if cfg!(feature = "compression") {
|
|
167
|
+
quote! {
|
|
168
|
+
fn compressed(_file_path: &str) -> ::std::option::Option<#crate_path::EmbeddedCompressedFile> {
|
|
169
|
+
::std::option::Option::None
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
TokenStream2::new()
|
|
174
|
+
};
|
|
120
175
|
let (handle_prefix, map_iter) = if let ::std::option::Option::Some(prefix) = prefix {
|
|
121
176
|
(
|
|
122
177
|
quote! { let file_path = file_path.strip_prefix(#prefix)?; },
|
|
@@ -221,6 +276,7 @@ fn dynamic(
|
|
|
221
276
|
|
|
222
277
|
#[cfg(debug_assertions)]
|
|
223
278
|
impl #crate_path::RustEmbed for #ident {
|
|
279
|
+
#dynamic_compressed
|
|
224
280
|
fn get(file_path: &str) -> ::std::option::Option<#crate_path::EmbeddedFile> {
|
|
225
281
|
#ident::get(file_path)
|
|
226
282
|
}
|
|
@@ -234,7 +290,7 @@ fn dynamic(
|
|
|
234
290
|
|
|
235
291
|
fn generate_assets(
|
|
236
292
|
ident: &syn::Ident, relative_folder_path: Option<&str>, absolute_folder_path: Option<String>, prefix: Option<String>, includes: Vec<String>, excludes: Vec<String>,
|
|
237
|
-
metadata_only: bool, crate_path: &syn::Path,
|
|
293
|
+
metadata_only: bool, crate_path: &syn::Path, compression: &str,
|
|
238
294
|
) -> syn::Result<TokenStream2> {
|
|
239
295
|
let embedded_impl = embedded(
|
|
240
296
|
ident,
|
|
@@ -245,6 +301,7 @@ fn generate_assets(
|
|
|
245
301
|
&excludes,
|
|
246
302
|
metadata_only,
|
|
247
303
|
crate_path,
|
|
304
|
+
compression,
|
|
248
305
|
);
|
|
249
306
|
if cfg!(feature = "debug-embed") {
|
|
250
307
|
return embedded_impl;
|
|
@@ -259,7 +316,7 @@ fn generate_assets(
|
|
|
259
316
|
}
|
|
260
317
|
|
|
261
318
|
fn embed_file(
|
|
262
|
-
folder_path: Option<&str>, ident: &syn::Ident, rel_path: &str, full_canonical_path: &str, metadata_only: bool, crate_path: &syn::Path,
|
|
319
|
+
folder_path: Option<&str>, ident: &syn::Ident, rel_path: &str, full_canonical_path: &str, metadata_only: bool, crate_path: &syn::Path, compression: &str,
|
|
263
320
|
) -> syn::Result<TokenStream2> {
|
|
264
321
|
let file = rust_embed_utils::read_file_from_fs(Path::new(full_canonical_path)).expect("File should be readable");
|
|
265
322
|
let hash = file.metadata.sha256_hash();
|
|
@@ -282,36 +339,37 @@ fn embed_file(
|
|
|
282
339
|
}
|
|
283
340
|
} else if cfg!(feature = "compression") {
|
|
284
341
|
let folder_path = folder_path.ok_or(syn::Error::new(ident.span(), "`folder` must be provided under `compression` feature."))?;
|
|
285
|
-
// Print some debugging information
|
|
286
342
|
let full_relative_path = PathBuf::from_iter([folder_path, rel_path]);
|
|
287
343
|
let full_relative_path = full_relative_path.to_string_lossy();
|
|
344
|
+
let compression_ident = format_ident!("{}", compression);
|
|
288
345
|
quote! {
|
|
289
|
-
#crate_path::flate!(static BYTES:
|
|
346
|
+
#crate_path::flate!(static BYTES: IFlate from #full_relative_path with #compression_ident);
|
|
290
347
|
}
|
|
291
348
|
} else {
|
|
292
349
|
quote! {
|
|
293
350
|
const BYTES: &'static [u8] = include_bytes!(#full_canonical_path);
|
|
294
351
|
}
|
|
295
352
|
};
|
|
296
|
-
|
|
353
|
+
Ok(if cfg!(feature = "compression") && !metadata_only {
|
|
297
|
-
quote! { ||
|
|
354
|
+
quote! { || {
|
|
355
|
+
#embedding_code
|
|
356
|
+
|
|
357
|
+
#crate_path::EmbeddedCompressedFile {
|
|
358
|
+
data: &*BYTES,
|
|
359
|
+
metadata: #crate_path::utils::__rust_embed_metadata!([#(#hash),*], #last_modified, #created, #crate_path::__mimetype_of!(#full_canonical_path))
|
|
360
|
+
}
|
|
361
|
+
} }
|
|
298
362
|
} else {
|
|
299
|
-
quote! {
|
|
363
|
+
quote! {
|
|
300
|
-
|
|
364
|
+
{
|
|
301
|
-
Ok(quote! {
|
|
302
|
-
#closure_args {
|
|
303
365
|
#embedding_code
|
|
304
366
|
|
|
305
367
|
#crate_path::EmbeddedFile {
|
|
306
|
-
data: ::std::borrow::Cow::Borrowed(
|
|
368
|
+
data: ::std::borrow::Cow::Borrowed(BYTES),
|
|
307
|
-
// The mimetype is computed by a proc macro (so it does not depend
|
|
308
|
-
// on the host's `mime-guess` feature) and passed as the final
|
|
309
|
-
// argument. `__rust_embed_metadata!` is selected target-side: when
|
|
310
|
-
// the target lacks `mime-guess` it drops this argument without ever
|
|
311
|
-
// expanding the `__mimetype_of!` call.
|
|
312
369
|
metadata: #crate_path::utils::__rust_embed_metadata!([#(#hash),*], #last_modified, #created, #crate_path::__mimetype_of!(#full_canonical_path))
|
|
313
370
|
}
|
|
314
371
|
}
|
|
372
|
+
}
|
|
315
373
|
})
|
|
316
374
|
}
|
|
317
375
|
|
|
@@ -367,6 +425,18 @@ fn impl_rust_embed(ast: &syn::DeriveInput) -> syn::Result<TokenStream2> {
|
|
|
367
425
|
}
|
|
368
426
|
let folder_path = folder_paths.remove(0);
|
|
369
427
|
|
|
428
|
+
const COMPRESSION_ALGOS: &[&str] = &["deflate", "zstd"];
|
|
429
|
+
let mut compression = find_attribute_values(ast, "compression");
|
|
430
|
+
if cfg!(feature = "compression") && (compression.len() > 1 || !compression.first().is_none_or(|v| COMPRESSION_ALGOS.contains(&v.as_str()))) {
|
|
431
|
+
return Err(syn::Error::new_spanned(
|
|
432
|
+
ast,
|
|
433
|
+
format!(
|
|
434
|
+
"#[derive(RustEmbed)] must contain one attribute like this #[compression = \"deflate\"]\navailable compression algorithm are {COMPRESSION_ALGOS:?}"
|
|
435
|
+
),
|
|
436
|
+
));
|
|
437
|
+
}
|
|
438
|
+
let compression = compression.pop().unwrap_or_else(|| "deflate".to_owned());
|
|
439
|
+
|
|
370
440
|
let prefix = find_attribute_values(ast, "prefix").into_iter().next();
|
|
371
441
|
let includes = find_attribute_values(ast, "include");
|
|
372
442
|
let excludes = find_attribute_values(ast, "exclude");
|
|
@@ -401,9 +471,17 @@ fn impl_rust_embed(ast: &syn::DeriveInput) -> syn::Result<TokenStream2> {
|
|
|
401
471
|
(Some(folder_path.clone()), absolute_path)
|
|
402
472
|
} else {
|
|
403
473
|
if cfg!(feature = "compression") {
|
|
474
|
+
let cargo_manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
|
|
475
|
+
match Path::new(&folder_path).strip_prefix(&cargo_manifest_dir) {
|
|
476
|
+
Ok(rel) => {
|
|
477
|
+
let rel_str = rel.to_str().unwrap().to_owned();
|
|
478
|
+
(Some(rel_str), folder_path)
|
|
479
|
+
}
|
|
404
|
-
|
|
480
|
+
Err(_) => return Err(syn::Error::new_spanned(ast, "`folder` must be a relative path under `compression` feature.")),
|
|
481
|
+
}
|
|
482
|
+
} else {
|
|
483
|
+
(None, folder_path)
|
|
405
484
|
}
|
|
406
|
-
(None, folder_path)
|
|
407
485
|
};
|
|
408
486
|
|
|
409
487
|
if !Path::new(&absolute_folder_path).exists() && !allow_missing {
|
|
@@ -436,10 +514,11 @@ fn impl_rust_embed(ast: &syn::DeriveInput) -> syn::Result<TokenStream2> {
|
|
|
436
514
|
excludes,
|
|
437
515
|
metadata_only,
|
|
438
516
|
&crate_path,
|
|
517
|
+
&compression,
|
|
439
518
|
)
|
|
440
519
|
}
|
|
441
520
|
|
|
442
|
-
#[proc_macro_derive(RustEmbed, attributes(folder, prefix, include, exclude, allow_missing, metadata_only, crate_path))]
|
|
521
|
+
#[proc_macro_derive(RustEmbed, attributes(folder, prefix, include, exclude, allow_missing, metadata_only, crate_path, compression))]
|
|
443
522
|
pub fn derive_input_object(input: TokenStream) -> TokenStream {
|
|
444
523
|
let ast = parse_macro_input!(input as DeriveInput);
|
|
445
524
|
match impl_rust_embed(&ast) {
|
src/lib.rs
CHANGED
|
@@ -3,9 +3,14 @@
|
|
|
3
3
|
#[cfg_attr(feature = "compression", doc(hidden))]
|
|
4
4
|
pub use include_flate::flate;
|
|
5
5
|
|
|
6
|
+
#[cfg(feature = "compression")]
|
|
7
|
+
pub use include_flate;
|
|
8
|
+
|
|
6
9
|
extern crate rust_embed_impl;
|
|
7
10
|
pub use rust_embed_impl::*;
|
|
8
11
|
|
|
12
|
+
#[cfg(feature = "compression")]
|
|
13
|
+
pub use rust_embed_utils::EmbeddedCompressedFile;
|
|
9
14
|
pub use rust_embed_utils::{EmbeddedFile, Metadata};
|
|
10
15
|
|
|
11
16
|
#[doc(hidden)]
|
|
@@ -28,6 +33,10 @@ pub extern crate rust_embed_utils as utils;
|
|
|
28
33
|
/// fn main() {}
|
|
29
34
|
/// ```
|
|
30
35
|
pub trait RustEmbed {
|
|
36
|
+
/// Get file compressed
|
|
37
|
+
#[cfg(feature = "compression")]
|
|
38
|
+
fn compressed(file_path: &str) -> Option<EmbeddedCompressedFile>;
|
|
39
|
+
|
|
31
40
|
/// Get an embedded file and its metadata.
|
|
32
41
|
///
|
|
33
42
|
/// If the feature `debug-embed` is enabled or the binary was compiled in
|
tests/compression.rs
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
use rust_embed::{Embed, EmbeddedCompressedFile, EmbeddedFile};
|
|
2
|
+
|
|
3
|
+
#[derive(Embed)]
|
|
4
|
+
#[folder = "examples/public/"]
|
|
5
|
+
struct Asset;
|
|
6
|
+
|
|
7
|
+
#[derive(Embed)]
|
|
8
|
+
#[folder = "examples/public/"]
|
|
9
|
+
#[metadata_only = true]
|
|
10
|
+
struct MetadataAsset;
|
|
11
|
+
|
|
12
|
+
#[test]
|
|
13
|
+
fn get_returns_decompressed_content() {
|
|
14
|
+
let file: EmbeddedFile = Asset::get("index.html").expect("index.html exists");
|
|
15
|
+
assert!(!file.data.is_empty());
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
#[test]
|
|
19
|
+
fn get_missing_returns_none() {
|
|
20
|
+
assert!(Asset::get("missing.html").is_none());
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
#[test]
|
|
24
|
+
fn metadata_only_compressed_always_returns_none() {
|
|
25
|
+
let file: Option<EmbeddedCompressedFile> = MetadataAsset::compressed("index.html");
|
|
26
|
+
assert!(file.is_none(), "metadata_only structs never have compressed data");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
#[test]
|
|
30
|
+
fn metadata_only_get_returns_empty_data() {
|
|
31
|
+
let file: EmbeddedFile = MetadataAsset::get("index.html").expect("index.html exists");
|
|
32
|
+
assert_eq!(file.data.len(), 0);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
#[cfg(any(not(debug_assertions), feature = "debug-embed"))]
|
|
36
|
+
mod release {
|
|
37
|
+
use super::*;
|
|
38
|
+
|
|
39
|
+
#[test]
|
|
40
|
+
fn compressed_exists() {
|
|
41
|
+
let file: Option<EmbeddedCompressedFile> = Asset::compressed("index.html");
|
|
42
|
+
assert!(file.is_some(), "index.html should have a compressed version");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
#[test]
|
|
46
|
+
fn compressed_missing_returns_none() {
|
|
47
|
+
let file: Option<EmbeddedCompressedFile> = Asset::compressed("missing.html");
|
|
48
|
+
assert!(file.is_none());
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
#[test]
|
|
52
|
+
fn compressed_decompresses_to_original_content() {
|
|
53
|
+
let compressed: EmbeddedCompressedFile = Asset::compressed("index.html").expect("index.html exists");
|
|
54
|
+
let uncompressed: EmbeddedFile = Asset::get("index.html").expect("index.html exists");
|
|
55
|
+
assert_eq!(compressed.data.decoded(), uncompressed.data.as_ref());
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
#[test]
|
|
59
|
+
fn compressed_and_get_share_same_hash() {
|
|
60
|
+
let compressed: EmbeddedCompressedFile = Asset::compressed("index.html").expect("index.html exists");
|
|
61
|
+
let uncompressed: EmbeddedFile = Asset::get("index.html").expect("index.html exists");
|
|
62
|
+
assert_eq!(compressed.metadata.sha256_hash(), uncompressed.metadata.sha256_hash());
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
#[test]
|
|
66
|
+
fn compressed_bytes_smaller_than_original_for_text() {
|
|
67
|
+
let compressed: EmbeddedCompressedFile = Asset::compressed("index.html").expect("index.html exists");
|
|
68
|
+
let uncompressed: EmbeddedFile = Asset::get("index.html").expect("index.html exists");
|
|
69
|
+
assert!(
|
|
70
|
+
compressed.data.compressed().len() < uncompressed.data.len(),
|
|
71
|
+
"compressed text should be smaller than the original"
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
#[test]
|
|
76
|
+
fn content_encoding_is_valid_http_header_value() {
|
|
77
|
+
let compressed: EmbeddedCompressedFile = Asset::compressed("index.html").expect("index.html exists");
|
|
78
|
+
let encoding = compressed.content_encoding();
|
|
79
|
+
assert!(
|
|
80
|
+
encoding == "deflate" || encoding == "zstd",
|
|
81
|
+
"content_encoding should be a known HTTP Content-Encoding value, got: {}",
|
|
82
|
+
encoding
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
#[test]
|
|
87
|
+
fn compressed_iter_matches_get_iter() {
|
|
88
|
+
let file_count = Asset::iter().count();
|
|
89
|
+
let compressed_count = Asset::iter().filter_map(|p| Asset::compressed(p.as_ref())).count();
|
|
90
|
+
assert_eq!(file_count, compressed_count, "every file should have a compressed version");
|
|
91
|
+
}
|
|
92
|
+
}
|
tests/custom_crate_path.rs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
//
|
|
1
|
+
// This test checks that the `crate_path` attribute can be used
|
|
2
|
-
//
|
|
2
|
+
// to specify a custom path to the `rust_embed` crate.
|
|
3
3
|
|
|
4
4
|
mod custom {
|
|
5
5
|
pub mod path {
|
|
@@ -17,4 +17,5 @@ mod rust_embed {}
|
|
|
17
17
|
#[derive(custom::path::rust_embed::RustEmbed)]
|
|
18
18
|
#[crate_path = "custom::path::rust_embed"]
|
|
19
19
|
#[folder = "examples/public/"]
|
|
20
|
+
#[allow(dead_code)]
|
|
20
21
|
struct Asset;
|
tests/path_traversal_attack.rs
CHANGED
|
@@ -14,6 +14,7 @@ fn path_traversal_attack_fails() {
|
|
|
14
14
|
|
|
15
15
|
#[derive(Embed)]
|
|
16
16
|
#[folder = "examples/axum-spa/"]
|
|
17
|
+
#[allow(dead_code)]
|
|
17
18
|
struct AxumAssets;
|
|
18
19
|
|
|
19
20
|
// TODO:
|
utils/Cargo.toml
CHANGED
|
@@ -13,7 +13,8 @@ edition = "2018"
|
|
|
13
13
|
|
|
14
14
|
[dependencies]
|
|
15
15
|
walkdir = "2.3.1"
|
|
16
|
-
sha2 = "0.
|
|
16
|
+
sha2 = "0.11"
|
|
17
|
+
include-flate = { version = "0.3.3", optional = true }
|
|
17
18
|
mime_guess = { version = "2.0.4", optional = true }
|
|
18
19
|
|
|
19
20
|
[dependencies.globset]
|
|
@@ -24,3 +25,4 @@ optional = true
|
|
|
24
25
|
debug-embed = []
|
|
25
26
|
mime-guess = ["mime_guess"]
|
|
26
27
|
include-exclude = ["globset"]
|
|
28
|
+
compression = ["include-flate"]
|
utils/src/lib.rs
CHANGED
|
@@ -44,6 +44,24 @@ pub struct EmbeddedFile {
|
|
|
44
44
|
pub metadata: Metadata,
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/// A file embedded into the binary compressed
|
|
48
|
+
#[cfg(feature = "compression")]
|
|
49
|
+
pub struct EmbeddedCompressedFile {
|
|
50
|
+
pub data: &'static include_flate::IFlate,
|
|
51
|
+
pub metadata: Metadata,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
#[cfg(feature = "compression")]
|
|
55
|
+
impl EmbeddedCompressedFile {
|
|
56
|
+
/// Returns the HTTP `Content-Encoding` header value for this file's compression algorithm.
|
|
57
|
+
pub fn content_encoding(&self) -> &'static str {
|
|
58
|
+
match self.data.algo() {
|
|
59
|
+
include_flate::CompressionMethod::Deflate => "deflate",
|
|
60
|
+
include_flate::CompressionMethod::Zstd => "zstd",
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
47
65
|
/// Metadata about an embedded file
|
|
48
66
|
#[derive(Clone)]
|
|
49
67
|
pub struct Metadata {
|