~repos /rust-embed
GIT_CONFIG_PARAMETERS="'http.version=HTTP/1.1'" git clone
https://git.pyrossh.dev/rust-embed/.git
rust-embed
Discussions:
https://groups.google.com/g/rust-embed-devs
rust macro which loads files into the rust binary at compile time during release and loads the file from the fs during dev.
Add support to get files dat compressed
- Cargo.toml +8 -3
- examples/axum.rs +36 -32
- impl/src/lib.rs +114 -29
- 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-impl/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
|
}
|
|
@@ -232,9 +288,10 @@ fn dynamic(
|
|
|
232
288
|
}
|
|
233
289
|
}
|
|
234
290
|
|
|
291
|
+
#[allow(clippy::too_many_arguments)]
|
|
235
292
|
fn generate_assets(
|
|
236
293
|
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,
|
|
294
|
+
metadata_only: bool, crate_path: &syn::Path, compression: &str,
|
|
238
295
|
) -> syn::Result<TokenStream2> {
|
|
239
296
|
let embedded_impl = embedded(
|
|
240
297
|
ident,
|
|
@@ -245,6 +302,7 @@ fn generate_assets(
|
|
|
245
302
|
&excludes,
|
|
246
303
|
metadata_only,
|
|
247
304
|
crate_path,
|
|
305
|
+
compression,
|
|
248
306
|
);
|
|
249
307
|
if cfg!(feature = "debug-embed") {
|
|
250
308
|
return embedded_impl;
|
|
@@ -259,7 +317,7 @@ fn generate_assets(
|
|
|
259
317
|
}
|
|
260
318
|
|
|
261
319
|
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,
|
|
320
|
+
folder_path: Option<&str>, ident: &syn::Ident, rel_path: &str, full_canonical_path: &str, metadata_only: bool, crate_path: &syn::Path, compression: &str,
|
|
263
321
|
) -> syn::Result<TokenStream2> {
|
|
264
322
|
let file = rust_embed_utils::read_file_from_fs(Path::new(full_canonical_path)).expect("File should be readable");
|
|
265
323
|
let hash = file.metadata.sha256_hash();
|
|
@@ -290,31 +348,37 @@ fn embed_file(
|
|
|
290
348
|
}
|
|
291
349
|
} else if cfg!(feature = "compression") {
|
|
292
350
|
let folder_path = folder_path.ok_or(syn::Error::new(ident.span(), "`folder` must be provided under `compression` feature."))?;
|
|
293
|
-
// Print some debugging information
|
|
294
351
|
let full_relative_path = PathBuf::from_iter([folder_path, rel_path]);
|
|
295
352
|
let full_relative_path = full_relative_path.to_string_lossy();
|
|
353
|
+
let compression_ident = format_ident!("{}", compression);
|
|
296
354
|
quote! {
|
|
297
|
-
#crate_path::flate!(static BYTES:
|
|
355
|
+
#crate_path::flate!(static BYTES: IFlate from #full_relative_path with #compression_ident);
|
|
298
356
|
}
|
|
299
357
|
} else {
|
|
300
358
|
quote! {
|
|
301
359
|
const BYTES: &'static [u8] = include_bytes!(#full_canonical_path);
|
|
302
360
|
}
|
|
303
361
|
};
|
|
304
|
-
|
|
362
|
+
Ok(if cfg!(feature = "compression") && !metadata_only {
|
|
305
|
-
quote! { ||
|
|
363
|
+
quote! { || {
|
|
364
|
+
#embedding_code
|
|
365
|
+
|
|
366
|
+
#crate_path::EmbeddedCompressedFile {
|
|
367
|
+
data: &*BYTES,
|
|
368
|
+
metadata: #crate_path::Metadata::__rust_embed_new([#(#hash),*], #last_modified, #created #mimetype_tokens)
|
|
369
|
+
}
|
|
370
|
+
} }
|
|
306
371
|
} else {
|
|
307
|
-
quote! {
|
|
372
|
+
quote! {
|
|
308
|
-
|
|
373
|
+
{
|
|
309
|
-
Ok(quote! {
|
|
310
|
-
#closure_args {
|
|
311
374
|
#embedding_code
|
|
312
375
|
|
|
313
376
|
#crate_path::EmbeddedFile {
|
|
314
|
-
data: ::std::borrow::Cow::Borrowed(
|
|
377
|
+
data: ::std::borrow::Cow::Borrowed(BYTES),
|
|
315
378
|
metadata: #crate_path::Metadata::__rust_embed_new([#(#hash),*], #last_modified, #created #mimetype_tokens)
|
|
316
379
|
}
|
|
317
380
|
}
|
|
381
|
+
}
|
|
318
382
|
})
|
|
319
383
|
}
|
|
320
384
|
|
|
@@ -370,6 +434,18 @@ fn impl_rust_embed(ast: &syn::DeriveInput) -> syn::Result<TokenStream2> {
|
|
|
370
434
|
}
|
|
371
435
|
let folder_path = folder_paths.remove(0);
|
|
372
436
|
|
|
437
|
+
const COMPRESSION_ALGOS: &[&str] = &["deflate", "zstd"];
|
|
438
|
+
let mut compression = find_attribute_values(ast, "compression");
|
|
439
|
+
if cfg!(feature = "compression") && (compression.len() > 1 || !compression.first().is_none_or(|v| COMPRESSION_ALGOS.contains(&v.as_str()))) {
|
|
440
|
+
return Err(syn::Error::new_spanned(
|
|
441
|
+
ast,
|
|
442
|
+
format!(
|
|
443
|
+
"#[derive(RustEmbed)] must contain one attribute like this #[compression = \"deflate\"]\navailable compression algorithm are {COMPRESSION_ALGOS:?}"
|
|
444
|
+
),
|
|
445
|
+
));
|
|
446
|
+
}
|
|
447
|
+
let compression = compression.pop().unwrap_or_else(|| "deflate".to_owned());
|
|
448
|
+
|
|
373
449
|
let prefix = find_attribute_values(ast, "prefix").into_iter().next();
|
|
374
450
|
let includes = find_attribute_values(ast, "include");
|
|
375
451
|
let excludes = find_attribute_values(ast, "exclude");
|
|
@@ -404,9 +480,17 @@ fn impl_rust_embed(ast: &syn::DeriveInput) -> syn::Result<TokenStream2> {
|
|
|
404
480
|
(Some(folder_path.clone()), absolute_path)
|
|
405
481
|
} else {
|
|
406
482
|
if cfg!(feature = "compression") {
|
|
483
|
+
let cargo_manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
|
|
484
|
+
match Path::new(&folder_path).strip_prefix(&cargo_manifest_dir) {
|
|
485
|
+
Ok(rel) => {
|
|
486
|
+
let rel_str = rel.to_str().unwrap().to_owned();
|
|
487
|
+
(Some(rel_str), folder_path)
|
|
488
|
+
}
|
|
407
|
-
|
|
489
|
+
Err(_) => return Err(syn::Error::new_spanned(ast, "`folder` must be a relative path under `compression` feature.")),
|
|
490
|
+
}
|
|
491
|
+
} else {
|
|
492
|
+
(None, folder_path)
|
|
408
493
|
}
|
|
409
|
-
(None, folder_path)
|
|
410
494
|
};
|
|
411
495
|
|
|
412
496
|
if !Path::new(&absolute_folder_path).exists() && !allow_missing {
|
|
@@ -439,10 +523,11 @@ fn impl_rust_embed(ast: &syn::DeriveInput) -> syn::Result<TokenStream2> {
|
|
|
439
523
|
excludes,
|
|
440
524
|
metadata_only,
|
|
441
525
|
&crate_path,
|
|
526
|
+
&compression,
|
|
442
527
|
)
|
|
443
528
|
}
|
|
444
529
|
|
|
445
|
-
#[proc_macro_derive(RustEmbed, attributes(folder, prefix, include, exclude, allow_missing, metadata_only, crate_path))]
|
|
530
|
+
#[proc_macro_derive(RustEmbed, attributes(folder, prefix, include, exclude, allow_missing, metadata_only, crate_path, compression))]
|
|
446
531
|
pub fn derive_input_object(input: TokenStream) -> TokenStream {
|
|
447
532
|
let ast = parse_macro_input!(input as DeriveInput);
|
|
448
533
|
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 {
|