Skip to main content

rust_embed_impl/
lib.rs

1#![recursion_limit = "1024"]
2#![forbid(unsafe_code)]
3#[macro_use]
4extern crate quote;
5extern crate proc_macro;
6
7use proc_macro::TokenStream;
8use proc_macro2::TokenStream as TokenStream2;
9use rust_embed_utils::PathMatcher;
10use std::{
11  collections::BTreeMap,
12  env,
13  io::ErrorKind,
14  iter::FromIterator,
15  path::{Path, PathBuf},
16};
17use syn::{parse_macro_input, Data, DeriveInput, Expr, ExprLit, Fields, Lit, Meta, MetaNameValue};
18
19#[allow(clippy::too_many_arguments)]
20fn embedded(
21  ident: &syn::Ident, relative_folder_path: Option<&str>, absolute_folder_path: Option<String>, prefix: Option<&str>, includes: &[String], excludes: &[String],
22  metadata_only: bool, crate_path: &syn::Path, compression: &str,
23) -> syn::Result<TokenStream2> {
24  extern crate rust_embed_utils;
25
26  let mut match_values = BTreeMap::new();
27  let mut list_values = Vec::<String>::new();
28
29  let includes: Vec<&str> = includes.iter().map(AsRef::as_ref).collect();
30  let excludes: Vec<&str> = excludes.iter().map(AsRef::as_ref).collect();
31  let matcher = PathMatcher::new(&includes, &excludes);
32  let entries = absolute_folder_path.clone()
33    .map(|v| rust_embed_utils::get_files(v, matcher))
34    .into_iter()
35    .flatten();
36  for rust_embed_utils::FileEntry { rel_path, full_canonical_path } in entries {
37    match_values.insert(
38      rel_path.clone(),
39      embed_file(relative_folder_path, ident, &rel_path, &full_canonical_path, metadata_only, crate_path, compression)?,
40    );
41
42    list_values.push(if let Some(prefix) = prefix {
43      format!("{}{}", prefix, rel_path)
44    } else {
45      rel_path
46    });
47  }
48
49  let array_len = list_values.len();
50
51  // If debug-embed is on, unconditionally include the code below. Otherwise,
52  // make it conditional on cfg(not(debug_assertions)).
53  let not_debug_attr = if cfg!(feature = "debug-embed") {
54    quote! {}
55  } else {
56    quote! { #[cfg(not(debug_assertions))]}
57  };
58
59  let handle_prefix = if let Some(prefix) = prefix {
60    quote! {
61      let file_path = file_path.strip_prefix(#prefix)?;
62    }
63  } else {
64    TokenStream2::new()
65  };
66  let match_values = match_values.into_iter().map(|(path, bytes)| {
67    quote! {
68        (#path, #bytes),
69    }
70  });
71  let use_compression = cfg!(feature = "compression") && !metadata_only;
72  let (entry_type, return_type) = if use_compression {
73    (
74      quote! { fn() -> #crate_path::EmbeddedCompressedFile },
75      quote! { #crate_path::EmbeddedCompressedFile },
76    )
77  } else {
78    (quote! { #crate_path::EmbeddedFile }, quote! { #crate_path::EmbeddedFile })
79  };
80  let get_value = if use_compression {
81    quote! {|idx| (ENTRIES[idx].1)()}
82  } else {
83    quote! {|idx| ENTRIES[idx].1.clone()}
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  };
125  Ok(quote! {
126      #not_debug_attr
127      impl #ident {
128          fn __file(file_path: &str) -> ::std::option::Option<#return_type> {
129              #handle_prefix
130              let key = file_path.replace("\\", "/");
131              static ENTRIES: &'static [(&'static str, #entry_type)] = &[
132                  #(#match_values)*];
133              let position = ENTRIES.binary_search_by_key(&key.as_str(), |entry| entry.0);
134              position.ok().map(#get_value)
135          }
136
137          #impls
138
139          fn names() -> ::std::slice::Iter<'static, &'static str> {
140              const ITEMS: [&str; #array_len] = [#(#list_values),*];
141              ITEMS.iter()
142          }
143
144          /// Iterates over the file paths in the folder.
145          pub fn iter() -> impl ::std::iter::Iterator<Item = ::std::borrow::Cow<'static, str>> + 'static {
146              Self::names().map(|x| ::std::borrow::Cow::from(*x))
147          }
148      }
149
150      #not_debug_attr
151      impl #crate_path::RustEmbed for #ident {
152        #compressed_trait_impl
153        fn get(file_path: &str) -> ::std::option::Option<#crate_path::EmbeddedFile> {
154          #ident::get(file_path)
155        }
156        fn iter() -> #crate_path::Filenames {
157          #crate_path::Filenames::Embedded(#ident::names())
158        }
159      }
160  })
161}
162
163fn dynamic(
164  ident: &syn::Ident, folder_path: Option<String>, prefix: Option<&str>, includes: &[String], excludes: &[String], metadata_only: bool, crate_path: &syn::Path,
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  };
175  let (handle_prefix, map_iter) = if let ::std::option::Option::Some(prefix) = prefix {
176    (
177      quote! { let file_path = file_path.strip_prefix(#prefix)?; },
178      quote! { ::std::borrow::Cow::Owned(format!("{}{}", #prefix, e.rel_path)) },
179    )
180  } else {
181    (TokenStream2::new(), quote! { ::std::borrow::Cow::from(e.rel_path) })
182  };
183
184  let declare_includes = quote! {
185    const INCLUDES: &[&str] = &[#(#includes),*];
186  };
187
188  let declare_excludes = quote! {
189    const EXCLUDES: &[&str] = &[#(#excludes),*];
190  };
191
192  // In metadata_only mode, we still need to read file contents to generate the
193  // file hash, but then we drop the file data.
194  let strip_contents = metadata_only.then_some(quote! {
195      .map(|mut file| { file.data = ::std::default::Default::default(); file })
196  });
197
198  let methods = if let Some(ref folder_path) = folder_path {
199    let non_canonical_folder_path = Path::new(&folder_path);
200    let canonical_folder_path = non_canonical_folder_path
201      .canonicalize()
202      .or_else(|err| match err {
203        err if err.kind() == ErrorKind::NotFound => Ok(non_canonical_folder_path.to_owned()),
204        err => Err(err),
205      })
206      .expect("folder path must resolve to an absolute path");
207    let canonical_folder_path = canonical_folder_path.to_str().expect("absolute folder path must be valid unicode");
208    quote! {
209          /// Get an embedded file and its metadata.
210          pub fn get(file_path: &str) -> ::std::option::Option<#crate_path::EmbeddedFile> {
211              #handle_prefix
212
213              let rel_file_path = file_path.replace("\\", "/");
214              let file_path = ::std::path::Path::new(#folder_path).join(&rel_file_path);
215
216              // Make sure the path requested does not escape the folder path
217              let canonical_file_path = file_path.canonicalize().ok()?;
218              if !canonical_file_path.starts_with(#canonical_folder_path) {
219                  // Tried to request a path that is not in the embedded folder
220
221                  // TODO: Currently it allows "path_traversal_attack" for the symlink files
222                  // For it to be working properly we need to get absolute path first
223                  // and check that instead if it starts with `canonical_folder_path`
224                  // https://doc.rust-lang.org/std/path/fn.absolute.html (currently nightly)
225                  // Should be allowed only if it was a symlink
226                  let metadata = ::std::fs::symlink_metadata(&file_path).ok()?;
227                  if !metadata.is_symlink() {
228                    return ::std::option::Option::None;
229                  }
230              }
231              let path_matcher = Self::matcher();
232              if path_matcher.is_path_included(&rel_file_path) {
233                #crate_path::utils::read_file_from_fs(&canonical_file_path).ok() #strip_contents
234              } else {
235                ::std::option::Option::None
236              }
237          }
238
239          /// Iterates over the file paths in the folder.
240          pub fn iter() -> impl ::std::iter::Iterator<Item = ::std::borrow::Cow<'static, str>> + 'static {
241              use ::std::path::Path;
242
243              #crate_path::utils::get_files(::std::string::String::from(#folder_path), Self::matcher())
244                  .map(|e| #map_iter)
245          }
246    }
247  } else {
248    quote! {
249          /// Get an embedded file and its metadata.
250          pub fn get(file_path: &str) -> ::std::option::Option<#crate_path::EmbeddedFile> {
251              // No folder_path defined so nothing can be returned.
252              None
253          }
254
255          /// Iterates over the file paths in the folder.
256          pub fn iter() -> impl ::std::iter::Iterator<Item = ::std::borrow::Cow<'static, str>> + 'static {
257              None.into_iter()
258          }
259    }
260  };
261
262  quote! {
263      #[cfg(debug_assertions)]
264      impl #ident {
265
266
267        fn matcher() -> #crate_path::utils::PathMatcher {
268            #declare_includes
269            #declare_excludes
270            static PATH_MATCHER: ::std::sync::OnceLock<#crate_path::utils::PathMatcher> = ::std::sync::OnceLock::new();
271            PATH_MATCHER.get_or_init(|| #crate_path::utils::PathMatcher::new(INCLUDES, EXCLUDES)).clone()
272        }
273
274          #methods
275      }
276
277      #[cfg(debug_assertions)]
278      impl #crate_path::RustEmbed for #ident {
279        #dynamic_compressed
280        fn get(file_path: &str) -> ::std::option::Option<#crate_path::EmbeddedFile> {
281          #ident::get(file_path)
282        }
283        fn iter() -> #crate_path::Filenames {
284          // the return type of iter() is unnamable, so we have to box it
285          #crate_path::Filenames::Dynamic(::std::boxed::Box::new(#ident::iter()))
286        }
287      }
288  }
289}
290
291fn generate_assets(
292  ident: &syn::Ident, relative_folder_path: Option<&str>, absolute_folder_path: Option<String>, prefix: Option<String>, includes: Vec<String>, excludes: Vec<String>,
293  metadata_only: bool, crate_path: &syn::Path, compression: &str,
294) -> syn::Result<TokenStream2> {
295  let embedded_impl = embedded(
296    ident,
297    relative_folder_path,
298    absolute_folder_path.clone(),
299    prefix.as_deref(),
300    &includes,
301    &excludes,
302    metadata_only,
303    crate_path,
304    compression,
305  );
306  if cfg!(feature = "debug-embed") {
307    return embedded_impl;
308  }
309  let embedded_impl = embedded_impl?;
310  let dynamic_impl = dynamic(ident, absolute_folder_path, prefix.as_deref(), &includes, &excludes, metadata_only, crate_path);
311
312  Ok(quote! {
313      #embedded_impl
314      #dynamic_impl
315  })
316}
317
318fn embed_file(
319  folder_path: Option<&str>, ident: &syn::Ident, rel_path: &str, full_canonical_path: &str, metadata_only: bool, crate_path: &syn::Path, compression: &str,
320) -> syn::Result<TokenStream2> {
321  let file = rust_embed_utils::read_file_from_fs(Path::new(full_canonical_path)).expect("File should be readable");
322  let hash = file.metadata.sha256_hash();
323  let (last_modified, created) = if cfg!(feature = "deterministic-timestamps") {
324    (quote! { ::std::option::Option::Some(0u64) }, quote! { ::std::option::Option::Some(0u64) })
325  } else {
326    let last_modified = match file.metadata.last_modified() {
327      Some(last_modified) => quote! { ::std::option::Option::Some(#last_modified) },
328      None => quote! { ::std::option::Option::None },
329    };
330    let created = match file.metadata.created() {
331      Some(created) => quote! { ::std::option::Option::Some(#created) },
332      None => quote! { ::std::option::Option::None },
333    };
334    (last_modified, created)
335  };
336  let embedding_code = if metadata_only {
337    quote! {
338        const BYTES: &'static [u8] = &[];
339    }
340  } else if cfg!(feature = "compression") {
341    let folder_path = folder_path.ok_or(syn::Error::new(ident.span(), "`folder` must be provided under `compression` feature."))?;
342    let full_relative_path = PathBuf::from_iter([folder_path, rel_path]);
343    let full_relative_path = full_relative_path.to_string_lossy();
344    let compression_ident = format_ident!("{}", compression);
345    quote! {
346      #crate_path::flate!(static BYTES: IFlate from #full_relative_path with #compression_ident);
347    }
348  } else {
349    quote! {
350      const BYTES: &'static [u8] = include_bytes!(#full_canonical_path);
351    }
352  };
353  Ok(if cfg!(feature = "compression") && !metadata_only {
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    } }
362  } else {
363    quote! {
364      {
365        #embedding_code
366
367        #crate_path::EmbeddedFile {
368            data: ::std::borrow::Cow::Borrowed(BYTES),
369            metadata: #crate_path::utils::__rust_embed_metadata!([#(#hash),*], #last_modified, #created, #crate_path::__mimetype_of!(#full_canonical_path))
370        }
371      }
372    }
373  })
374}
375
376/// Find all pairs of the `name = "value"` attribute from the derive input
377fn find_attribute_values(ast: &syn::DeriveInput, attr_name: &str) -> Vec<String> {
378  ast
379    .attrs
380    .iter()
381    .filter(|value| value.path().is_ident(attr_name))
382    .filter_map(|attr| match &attr.meta {
383      Meta::NameValue(MetaNameValue {
384        value: Expr::Lit(ExprLit { lit: Lit::Str(val), .. }),
385        ..
386      }) => Some(val.value()),
387      _ => None,
388    })
389    .collect()
390}
391
392fn find_bool_attribute(ast: &syn::DeriveInput, attr_name: &str) -> Option<bool> {
393  ast
394    .attrs
395    .iter()
396    .find(|value| value.path().is_ident(attr_name))
397    .and_then(|attr| match &attr.meta {
398      Meta::NameValue(MetaNameValue {
399        value: Expr::Lit(ExprLit { lit: Lit::Bool(val), .. }),
400        ..
401      }) => Some(val.value()),
402      _ => None,
403    })
404}
405
406fn impl_rust_embed(ast: &syn::DeriveInput) -> syn::Result<TokenStream2> {
407  match ast.data {
408    Data::Struct(ref data) => match data.fields {
409      Fields::Unit => {}
410      _ => return Err(syn::Error::new_spanned(ast, "RustEmbed can only be derived for unit structs")),
411    },
412    _ => return Err(syn::Error::new_spanned(ast, "RustEmbed can only be derived for unit structs")),
413  };
414
415  let crate_path: syn::Path = find_attribute_values(ast, "crate_path")
416    .last()
417    .map_or_else(|| syn::parse_str("rust_embed").unwrap(), |v| syn::parse_str(v).unwrap());
418
419  let mut folder_paths = find_attribute_values(ast, "folder");
420  if folder_paths.len() != 1 {
421    return Err(syn::Error::new_spanned(
422      ast,
423      "#[derive(RustEmbed)] must contain one attribute like this #[folder = \"examples/public/\"]",
424    ));
425  }
426  let folder_path = folder_paths.remove(0);
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
440  let prefix = find_attribute_values(ast, "prefix").into_iter().next();
441  let includes = find_attribute_values(ast, "include");
442  let excludes = find_attribute_values(ast, "exclude");
443  let metadata_only = find_bool_attribute(ast, "metadata_only").unwrap_or(false);
444  let allow_missing = find_bool_attribute(ast, "allow_missing").unwrap_or(false);
445
446  #[cfg(not(feature = "include-exclude"))]
447  if !includes.is_empty() || !excludes.is_empty() {
448    return Err(syn::Error::new_spanned(
449      ast,
450      "Please turn on the `include-exclude` feature to use the `include` and `exclude` attributes",
451    ));
452  }
453
454  #[cfg(feature = "interpolate-folder-path")]
455  let folder_path = match shellexpand::full(&folder_path) {
456    Ok(v) => Ok(Some(v.to_string())),
457    Err(v) if v.cause == std::env::VarError::NotPresent && allow_missing => Ok(None),
458    Err(v) => Err(syn::Error::new_spanned(ast, v.to_string())),
459  }?;
460  #[cfg(not(feature = "interpolate-folder-path"))]
461  let folder_path = Some(folder_path);
462
463  // Base relative paths on the Cargo.toml location
464  let (relative_path, absolute_folder_path) = if let Some(folder_path) = folder_path {
465    let (relative_path, absolute_folder_path) = if Path::new(&folder_path).is_relative() {
466      let absolute_path = Path::new(&env::var("CARGO_MANIFEST_DIR").unwrap())
467        .join(&folder_path)
468        .to_str()
469        .unwrap()
470        .to_owned();
471      (Some(folder_path.clone()), absolute_path)
472    } else {
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          }
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)
484      }
485    };
486
487    if !Path::new(&absolute_folder_path).exists() && !allow_missing {
488      let mut message = format!(
489        "#[derive(RustEmbed)] folder '{}' does not exist. cwd: '{}'",
490        absolute_folder_path,
491        std::env::current_dir().unwrap().to_str().unwrap()
492      );
493
494      // Add a message about the interpolate-folder-path feature if the path may
495      // include a variable
496      if absolute_folder_path.contains('$') && cfg!(not(feature = "interpolate-folder-path")) {
497        message += "\nA variable has been detected. RustEmbed can expand variables \
498                    when the `interpolate-folder-path` feature is enabled.";
499      }
500
501      return Err(syn::Error::new_spanned(ast, message));
502    };
503    (relative_path, Some(absolute_folder_path))
504  } else {
505    (None, None)
506  };
507
508  generate_assets(
509    &ast.ident,
510    relative_path.as_deref(),
511    absolute_folder_path,
512    prefix,
513    includes,
514    excludes,
515    metadata_only,
516    &crate_path,
517    &compression,
518  )
519}
520
521#[proc_macro_derive(RustEmbed, attributes(folder, prefix, include, exclude, allow_missing, metadata_only, crate_path, compression))]
522pub fn derive_input_object(input: TokenStream) -> TokenStream {
523  let ast = parse_macro_input!(input as DeriveInput);
524  match impl_rust_embed(&ast) {
525    Ok(ok) => ok.into(),
526    Err(e) => e.to_compile_error().into(),
527  }
528}
529
530/// Expands to a string literal containing the mimetype of the file at the given
531/// path, guessed from its extension.
532///
533/// This lives in the proc-macro crate (which depends on `mime_guess`
534/// unconditionally) rather than being computed inside the derive's host-side
535/// logic: the derive must emit tokens that do not depend on the *host*'s
536/// `mime-guess` feature, and this proc macro is invoked only from the
537/// target-side `__rust_embed_metadata!` macro. When the target has `mime-guess`
538/// disabled, the invocation generated by the derive is discarded by that macro
539/// without ever being expanded.
540#[doc(hidden)]
541#[proc_macro]
542pub fn __mimetype_of(input: TokenStream) -> TokenStream {
543  let path = parse_macro_input!(input as syn::LitStr).value();
544  let mimetype = mime_guess::from_path(&path).first_or_octet_stream().to_string();
545  quote! { #mimetype }.into()
546}