Skip to main content

embed_impl/
embed_impl.rs

1//! Implementation of the `Embed` derive macro for the `embed` crate.
2//!
3//! This crate is an implementation detail and should not be used directly.
4#![recursion_limit = "1024"]
5#![forbid(unsafe_code)]
6#[macro_use]
7extern crate quote;
8extern crate proc_macro;
9
10use std::{
11    collections::BTreeMap,
12    env,
13    iter::FromIterator,
14    path::{Path, PathBuf},
15};
16
17use embed_utils::PathMatcher;
18use proc_macro::TokenStream;
19use proc_macro2::TokenStream as TokenStream2;
20use syn::{Data, DeriveInput, Expr, ExprLit, Fields, Lit, Meta, MetaNameValue, parse_macro_input};
21
22fn embedded(
23    ident: &syn::Ident,
24    relative_folder_path: Option<&str>,
25    absolute_folder_path: &Path,
26    prefix: Option<&str>,
27    includes: &[String],
28    excludes: &[String],
29    metadata_only: bool,
30    crate_path: &syn::Path,
31) -> syn::Result<TokenStream2> {
32    extern crate embed_utils;
33
34    let mut match_values = BTreeMap::new();
35    let mut list_values = Vec::<String>::new();
36
37    let includes: Vec<&str> = includes.iter().map(AsRef::as_ref).collect();
38    let excludes: Vec<&str> = excludes.iter().map(AsRef::as_ref).collect();
39    let matcher = PathMatcher::new(&includes, &excludes);
40    for embed_utils::FileEntry {
41        rel_path,
42        full_canonical_path,
43    } in embed_utils::get_files(absolute_folder_path, &matcher)
44    {
45        match_values.insert(
46            rel_path.clone(),
47            embed_file(
48                relative_folder_path,
49                ident,
50                &rel_path,
51                &full_canonical_path,
52                metadata_only,
53                crate_path,
54            )?,
55        );
56
57        list_values.push(if let Some(prefix) = prefix {
58            format!("{}{}", prefix, rel_path)
59        } else {
60            rel_path
61        });
62    }
63
64    let array_len = list_values.len();
65
66    // If debug-embed is on, unconditionally include the code below. Otherwise,
67    // make it conditional on cfg(not(debug_assertions)).
68    let not_debug_attr = if cfg!(feature = "debug-embed") {
69        quote! {}
70    } else {
71        quote! { #[cfg(not(debug_assertions))]}
72    };
73
74    let handle_prefix = if let Some(prefix) = prefix {
75        quote! {
76          let file_path = match file_path.strip_prefix(#prefix) {
77            ::std::option::Option::Some(file_path) => file_path,
78            ::std::option::Option::None => return ::std::option::Option::None,
79          };
80        }
81    } else {
82        TokenStream2::new()
83    };
84    let match_values = match_values.into_iter().map(|(path, bytes)| {
85        quote! {
86            (#path, #bytes),
87        }
88    });
89    let value_type = if cfg!(feature = "compression") {
90        quote! { fn() -> #crate_path::EmbeddedFile }
91    } else {
92        quote! { #crate_path::EmbeddedFile }
93    };
94    let get_value = if cfg!(feature = "compression") {
95        quote! {|idx| (ENTRIES[idx].1)()}
96    } else {
97        quote! {|idx| ENTRIES[idx].1.clone()}
98    };
99    Ok(quote! {
100        #not_debug_attr
101        impl #ident {
102            /// Get an embedded file and its metadata.
103            pub fn get(file_path: &str) -> ::std::option::Option<#crate_path::EmbeddedFile> {
104              #handle_prefix
105              const ENTRIES: &'static [(&'static str, #value_type)] = &[
106                  #(#match_values)*];
107              let position = ENTRIES.binary_search_by_key(&file_path, |entry| entry.0);
108              position.ok().map(#get_value)
109
110            }
111
112            fn names() -> ::std::slice::Iter<'static, &'static str> {
113                const ITEMS: [&str; #array_len] = [#(#list_values),*];
114                ITEMS.iter()
115            }
116
117            /// Iterates over the file paths in the folder.
118            pub fn iter() -> impl ::std::iter::Iterator<Item = ::std::borrow::Cow<'static, str>> {
119                Self::names().map(|x| ::std::borrow::Cow::from(*x))
120            }
121        }
122
123        #not_debug_attr
124        impl #crate_path::RustEmbed for #ident {
125          fn get(file_path: &str) -> ::std::option::Option<#crate_path::EmbeddedFile> {
126            #ident::get(file_path)
127          }
128          fn iter() -> #crate_path::Filenames {
129            #crate_path::Filenames::Embedded(#ident::names())
130          }
131        }
132    })
133}
134
135fn dynamic(
136    ident: &syn::Ident,
137    folder_paths: Vec<String>,
138    prefix: Option<&str>,
139    includes: &[String],
140    excludes: &[String],
141    metadata_only: bool,
142    crate_path: &syn::Path,
143) -> TokenStream2 {
144    let (handle_prefix, map_iter) = if let ::std::option::Option::Some(prefix) = prefix {
145        (
146            quote! { let file_path = file_path.strip_prefix(#prefix)?; },
147            quote! { ::std::borrow::Cow::Owned(format!("{}{}", #prefix, e.rel_path)) },
148        )
149    } else {
150        (TokenStream2::new(), quote! { ::std::borrow::Cow::from(e.rel_path) })
151    };
152
153    let declare_includes = quote! {
154      const INCLUDES: &[&str] = &[#(#includes),*];
155    };
156
157    let declare_excludes = quote! {
158      const EXCLUDES: &[&str] = &[#(#excludes),*];
159    };
160
161    // In metadata_only mode, we still need to read file contents to generate the
162    // file hash, but then we drop the file data.
163    let strip_contents = metadata_only.then_some(quote! {
164        .map(|mut file| { file.data = ::std::default::Default::default(); file })
165    });
166
167    // The assets folder is resolved at runtime from the given candidates, in
168    // order. `folder_paths[0]` is the absolute path captured at build time, so
169    // on a machine where the build directory is still present the previous
170    // behaviour is unchanged. The remaining candidates are the path as written
171    // in `#[folder]`, tried relative to the directory the binary is run from,
172    // which keeps debug builds working when the build directory is unavailable
173    // (for example inside a WASI sandbox).
174    quote! {
175        #[cfg(debug_assertions)]
176        impl #ident {
177
178
179          fn matcher() -> &'static #crate_path::utils::PathMatcher {
180              #declare_includes
181              #declare_excludes
182              static PATH_MATCHER: ::std::sync::OnceLock<#crate_path::utils::PathMatcher> = ::std::sync::OnceLock::new();
183              PATH_MATCHER.get_or_init(|| #crate_path::utils::PathMatcher::new(INCLUDES, EXCLUDES))
184          }
185
186          /// Resolves the assets folder at runtime, caching the result.
187          ///
188          /// Returns `None` if neither the build time path nor the path relative
189          /// to the current directory exists.
190          fn folder_root() -> ::std::option::Option<&'static ::std::path::Path> {
191              static FOLDER_ROOT: ::std::sync::OnceLock<::std::option::Option<::std::path::PathBuf>> = ::std::sync::OnceLock::new();
192              FOLDER_ROOT
193                  .get_or_init(|| {
194                      [#(::std::path::Path::new(#folder_paths)),*]
195                          .into_iter()
196                          .find_map(|candidate| candidate.canonicalize().ok())
197                  })
198                  .as_deref()
199          }
200
201            /// Get an embedded file and its metadata.
202            ///
203            /// Returns `None` when the path is not a valid relative path, when
204            /// the folder cannot be resolved, when the file does not exist or
205            /// when the requested path would leave the folder (for example
206            /// through a symbolic link).
207            pub fn get(file_path: &str) -> ::std::option::Option<#crate_path::EmbeddedFile> {
208                #handle_prefix
209
210                // Only accept paths that could have been returned by `iter`.
211                // This keeps debug builds behaving exactly like release builds,
212                // which look the path up in a static table of relative paths.
213                if !#crate_path::utils::is_valid_key(file_path) {
214                    return ::std::option::Option::None;
215                }
216
217                let rel_file_path = file_path;
218                let folder_root = Self::folder_root()?;
219                let file_path = folder_root.join(rel_file_path);
220
221                // Make sure the path requested does not escape the folder path,
222                // even through a symbolic link.
223                let canonical_file_path = file_path.canonicalize().ok()?;
224                if !canonical_file_path.starts_with(folder_root) {
225                    return ::std::option::Option::None;
226                }
227
228                let path_matcher = Self::matcher();
229                if path_matcher.is_path_included(rel_file_path) {
230                  #crate_path::utils::read_file_from_fs(&canonical_file_path).ok() #strip_contents
231                } else {
232                  ::std::option::Option::None
233                }
234            }
235
236            /// Iterates over the file paths in the folder.
237            pub fn iter() -> impl ::std::iter::Iterator<Item = ::std::borrow::Cow<'static, str>> {
238                Self::folder_root()
239                    .into_iter()
240                    .flat_map(|folder_root| {
241                        #crate_path::utils::get_files(folder_root, Self::matcher())
242                    })
243                    .map(|e| #map_iter)
244            }
245        }
246
247        #[cfg(debug_assertions)]
248        impl #crate_path::RustEmbed for #ident {
249          fn get(file_path: &str) -> ::std::option::Option<#crate_path::EmbeddedFile> {
250            #ident::get(file_path)
251          }
252          fn iter() -> #crate_path::Filenames {
253            // the return type of iter() is unnamable, so we have to box it
254            #crate_path::Filenames::Dynamic(::std::boxed::Box::new(#ident::iter()))
255          }
256        }
257    }
258}
259
260fn generate_assets(
261    ident: &syn::Ident,
262    relative_folder_path: Option<&str>,
263    absolute_folder_path: String,
264    prefix: Option<String>,
265    includes: Vec<String>,
266    excludes: Vec<String>,
267    metadata_only: bool,
268    crate_path: &syn::Path,
269) -> syn::Result<TokenStream2> {
270    let embedded_impl = embedded(
271        ident,
272        relative_folder_path,
273        Path::new(&absolute_folder_path),
274        prefix.as_deref(),
275        &includes,
276        &excludes,
277        metadata_only,
278        crate_path,
279    );
280    if cfg!(feature = "debug-embed") {
281        return embedded_impl;
282    }
283    let embedded_impl = embedded_impl?;
284    // Resolve the folder at runtime from the build time absolute path first,
285    // then from the path as written in `#[folder]` relative to the directory
286    // the binary is run from.
287    let fallback_folder_path = relative_folder_path.unwrap_or(absolute_folder_path.as_str()).to_owned();
288    let dynamic_impl = dynamic(
289        ident,
290        vec![absolute_folder_path, fallback_folder_path],
291        prefix.as_deref(),
292        &includes,
293        &excludes,
294        metadata_only,
295        crate_path,
296    );
297
298    Ok(quote! {
299        #embedded_impl
300        #dynamic_impl
301    })
302}
303
304fn embed_file(
305    folder_path: Option<&str>,
306    ident: &syn::Ident,
307    rel_path: &str,
308    full_canonical_path: &Path,
309    metadata_only: bool,
310    crate_path: &syn::Path,
311) -> syn::Result<TokenStream2> {
312    let full_canonical_path = full_canonical_path.to_str().ok_or_else(|| {
313        syn::Error::new(
314            ident.span(),
315            format!("`{}` does not have a UTF-8 representation", full_canonical_path.display()),
316        )
317    })?;
318
319    let file = embed_utils::read_file_from_fs(Path::new(full_canonical_path)).map_err(|error| {
320        syn::Error::new(ident.span(), format!("could not read `{}`: {}", full_canonical_path, error))
321    })?;
322    let hash = file.metadata.hash();
323    let last_modified = match file.metadata.last_modified() {
324        Some(last_modified) => quote! { ::std::option::Option::Some(#last_modified) },
325        None => quote! { ::std::option::Option::None },
326    };
327    let created = match file.metadata.created() {
328        Some(created) => quote! { ::std::option::Option::Some(#created) },
329        None => quote! { ::std::option::Option::None },
330    };
331    // The mimetype is always emitted. Whether it is kept is decided on the
332    // target by `__embed_metadata!`, because this proc-macro is compiled for
333    // the host and its features are resolved independently of the target.
334    let mimetype = embed_utils::mimetype_of(Path::new(full_canonical_path));
335    let mimetype = mimetype.as_str();
336
337    let embedding_code = if metadata_only {
338        quote! {
339            const BYTES: &'static [u8] = &[];
340        }
341    } else if cfg!(feature = "compression") {
342        let folder_path = folder_path.ok_or(syn::Error::new(
343            ident.span(),
344            "`folder` must be provided under `compression` feature.",
345        ))?;
346        // Print some debugging information
347        let full_relative_path = PathBuf::from_iter([folder_path, rel_path]);
348        let full_relative_path = full_relative_path.to_string_lossy();
349        quote! {
350          #crate_path::flate!(static BYTES: [u8] from #full_relative_path);
351        }
352    } else {
353        quote! {
354          const BYTES: &'static [u8] = include_bytes!(#full_canonical_path);
355        }
356    };
357    let closure_args = if cfg!(feature = "compression") {
358        quote! { || }
359    } else {
360        quote! {}
361    };
362    Ok(quote! {
363         #closure_args {
364          #embedding_code
365
366          #crate_path::EmbeddedFile {
367              data: ::std::borrow::Cow::Borrowed(&BYTES),
368              metadata: #crate_path::utils::__embed_metadata!([#(#hash),*], #last_modified, #created, #mimetype)
369          }
370        }
371    })
372}
373
374/// Find all pairs of the `name = "value"` attribute from the derive input
375fn find_attribute_values(ast: &syn::DeriveInput, attr_name: &str) -> Vec<String> {
376    ast.attrs
377        .iter()
378        .filter(|value| value.path().is_ident(attr_name))
379        .filter_map(|attr| match &attr.meta {
380            Meta::NameValue(MetaNameValue {
381                value: Expr::Lit(ExprLit {
382                    lit: Lit::Str(val), ..
383                }),
384                ..
385            }) => Some(val.value()),
386            _ => None,
387        })
388        .collect()
389}
390
391fn find_bool_attribute(ast: &syn::DeriveInput, attr_name: &str) -> Option<bool> {
392    ast.attrs
393        .iter()
394        .find(|value| value.path().is_ident(attr_name))
395        .and_then(|attr| match &attr.meta {
396            Meta::NameValue(MetaNameValue {
397                value: Expr::Lit(ExprLit {
398                    lit: Lit::Bool(val), ..
399                }),
400                ..
401            }) => Some(val.value()),
402            _ => None,
403        })
404}
405
406fn impl_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 = match find_attribute_values(ast, "crate_path").last() {
416        Some(value) => syn::parse_str(value)
417            .map_err(|error| syn::Error::new_spanned(ast, format!("invalid `crate_path` value `{value}`: {error}")))?,
418        None => syn::parse_str("embed").expect("`embed` is a valid path"),
419    };
420
421    let mut folder_paths = find_attribute_values(ast, "folder");
422    if folder_paths.len() != 1 {
423        return Err(syn::Error::new_spanned(
424            ast,
425            "#[derive(RustEmbed)] must contain one attribute like this #[folder = \"examples/public/\"]",
426        ));
427    }
428    let folder_path = folder_paths.remove(0);
429
430    let prefix = find_attribute_values(ast, "prefix").into_iter().next();
431    let includes = find_attribute_values(ast, "include");
432    let excludes = find_attribute_values(ast, "exclude");
433    let metadata_only = find_bool_attribute(ast, "metadata_only").unwrap_or(false);
434
435    #[cfg(not(feature = "include-exclude"))]
436    if !includes.is_empty() || !excludes.is_empty() {
437        return Err(syn::Error::new_spanned(
438            ast,
439            "Please turn on the `include-exclude` feature to use the `include` and `exclude` attributes",
440        ));
441    }
442
443    #[cfg(feature = "interpolate-folder-path")]
444    let folder_path = shellexpand::full(&folder_path)
445        .map_err(|v| syn::Error::new_spanned(ast, v.to_string()))?
446        .to_string();
447
448    // Base relative paths on the Cargo.toml location
449    let (relative_path, absolute_folder_path) = if Path::new(&folder_path).is_relative() {
450        let manifest_dir = env::var("CARGO_MANIFEST_DIR")
451            .map_err(|_| syn::Error::new_spanned(ast, "the CARGO_MANIFEST_DIR environment variable is not set"))?;
452        let absolute_path = Path::new(&manifest_dir).join(&folder_path);
453        let absolute_path = absolute_path
454            .to_str()
455            .ok_or_else(|| syn::Error::new_spanned(ast, "the `folder` path is not valid UTF-8"))?
456            .to_owned();
457        (Some(folder_path.clone()), absolute_path)
458    } else {
459        if cfg!(feature = "compression") {
460            return Err(syn::Error::new_spanned(
461                ast,
462                "`folder` must be a relative path under `compression` feature.",
463            ));
464        }
465        (None, folder_path)
466    };
467
468    let absolute_path = Path::new(&absolute_folder_path);
469    if !absolute_path.exists() {
470        let cwd = env::current_dir()
471            .ok()
472            .map(|path| path.to_string_lossy().into_owned())
473            .unwrap_or_else(|| "<unknown>".to_owned());
474        let mut message = format!(
475            "#[derive(RustEmbed)] folder '{}' does not exist. cwd: '{}'",
476            absolute_folder_path, cwd
477        );
478
479        // Add a message about the interpolate-folder-path feature if the path may
480        // include a variable
481        if absolute_folder_path.contains('$') && cfg!(not(feature = "interpolate-folder-path")) {
482            message += "\nA variable has been detected. RustEmbed can expand variables \
483                  when the `interpolate-folder-path` feature is enabled.";
484        }
485
486        return Err(syn::Error::new_spanned(ast, message));
487    }
488    if !absolute_path.is_dir() {
489        return Err(syn::Error::new_spanned(
490            ast,
491            format!("#[derive(RustEmbed)] folder '{}' is not a directory", absolute_folder_path),
492        ));
493    }
494
495    generate_assets(
496        &ast.ident,
497        relative_path.as_deref(),
498        absolute_folder_path,
499        prefix,
500        includes,
501        excludes,
502        metadata_only,
503        &crate_path,
504    )
505}
506
507#[proc_macro_derive(RustEmbed, attributes(folder, prefix, include, exclude, metadata_only, crate_path))]
508pub fn derive_input_object(input: TokenStream) -> TokenStream {
509    let ast = parse_macro_input!(input as DeriveInput);
510    match impl_embed(&ast) {
511        Ok(ok) => ok.into(),
512        Err(e) => e.to_compile_error().into(),
513    }
514}
515
516#[cfg(test)]
517mod tests {
518    use super::*;
519
520    fn derive(input: &str) -> syn::Result<TokenStream2> {
521        impl_embed(&syn::parse_str::<syn::DeriveInput>(input).expect("valid derive input"))
522    }
523
524    #[test]
525    fn a_folder_that_is_a_directory_is_accepted() {
526        // Resolved relative to this crate's manifest directory.
527        assert!(derive(r#"#[folder = "."] struct Assets;"#).is_ok());
528    }
529
530    #[test]
531    fn a_missing_folder_is_a_clean_error() {
532        let error = derive(r#"#[folder = "definitely-not-here"] struct Assets;"#).unwrap_err();
533        assert!(error.to_string().contains("does not exist"), "{}", error);
534    }
535
536    #[test]
537    fn a_folder_that_is_a_file_is_a_clean_error() {
538        let error = derive(r#"#[folder = "Cargo.toml"] struct Assets;"#).unwrap_err();
539        assert!(error.to_string().contains("not a directory"), "{}", error);
540    }
541
542    #[test]
543    fn the_folder_attribute_is_required_exactly_once() {
544        for input in ["struct Assets;", r#"#[folder = "."] #[folder = "."] struct Assets;"#] {
545            let error = derive(input).unwrap_err();
546            assert!(error.to_string().contains("one attribute"), "{}", error);
547        }
548    }
549
550    #[test]
551    fn an_invalid_crate_path_is_a_clean_error() {
552        let error = derive(r#"#[folder = "."] #[crate_path = "not a path!!"] struct Assets;"#).unwrap_err();
553        assert!(error.to_string().contains("crate_path"), "{}", error);
554    }
555
556    #[test]
557    fn only_unit_structs_are_supported() {
558        let error = derive(r#"#[folder = "."] struct Assets { field: u8 }"#).unwrap_err();
559        assert!(error.to_string().contains("unit structs"), "{}", error);
560    }
561
562    #[test]
563    fn a_wrongly_typed_metadata_only_is_ignored() {
564        // `metadata_only` without a boolean literal falls back to `false`.
565        assert!(derive(r#"#[folder = "."] #[metadata_only = "yes"] struct Assets;"#).is_ok());
566    }
567}