~repos /rust-embed

#rust#proc-macro#http

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.


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.70.0"
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.10"
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::{header, StatusCode},
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
- static_handler(Path("index.html".to_string())).await
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
- StaticFile(path)
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
- #[derive(Embed)]
42
+ fn serve_asset(path: &str, headers: &HeaderMap) -> Response {
45
- #[folder = "examples/public/"]
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
- fn into_response(self) -> Response {
44
+ if let Some(compressed) = Asset::compressed(path) {
55
- let path = self.0.into();
45
+ let encoding = compressed.content_encoding();
56
-
57
- match Asset::get(path.as_str()) {
46
+ let accept = headers.get(axum::http::header::ACCEPT_ENCODING).and_then(|v| v.to_str().ok()).unwrap_or("");
58
- Some(content) => {
47
+ if accept.contains(encoding) {
48
+ return (
49
+ [
59
- let mime = mime_guess::from_path(path).first_or_octet_stream();
50
+ #[cfg(feature = "mime-guess")]
60
- ([(header::CONTENT_TYPE, mime.as_ref())], content.data).into_response()
51
+ (axum::http::header::CONTENT_TYPE, compressed.metadata.mimetype()),
61
- }
62
- None => (StatusCode::NOT_FOUND, "404 Not Found").into_response(),
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 value_type = if cfg!(feature = "compression") {
71
+ let use_compression = cfg!(feature = "compression") && !metadata_only;
72
+ let (entry_type, return_type) = if use_compression {
73
+ (
71
- quote! { fn() -> #crate_path::EmbeddedFile }
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 cfg!(feature = "compression") {
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
- pub fn get(file_path: &str) -> ::std::option::Option<#crate_path::EmbeddedFile> {
128
+ fn __file(file_path: &str) -> ::std::option::Option<#return_type> {
85
- #handle_prefix
129
+ #handle_prefix
86
- let key = file_path.replace("\\", "/");
130
+ let key = file_path.replace("\\", "/");
87
- static ENTRIES: &'static [(&'static str, #value_type)] = &[
131
+ static ENTRIES: &'static [(&'static str, #entry_type)] = &[
88
- #(#match_values)*];
132
+ #(#match_values)*];
89
- let position = ENTRIES.binary_search_by_key(&key.as_str(), |entry| entry.0);
133
+ let position = ENTRIES.binary_search_by_key(&key.as_str(), |entry| entry.0);
90
- position.ok().map(#get_value)
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: [u8] from #full_relative_path);
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
- let closure_args = if cfg!(feature = "compression") {
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(&BYTES),
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
- return Err(syn::Error::new_spanned(ast, "`folder` must be a relative path under `compression` feature."));
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
- /// This test checks that the `crate_path` attribute can be used
1
+ // This test checks that the `crate_path` attribute can be used
2
- /// to specify a custom path to the `rust_embed` crate.
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.10.5"
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 {