1#![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 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 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 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 let strip_contents = metadata_only.then_some(quote! {
164 .map(|mut file| { file.data = ::std::default::Default::default(); file })
165 });
166
167 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 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 pub fn get(file_path: &str) -> ::std::option::Option<#crate_path::EmbeddedFile> {
208 #handle_prefix
209
210 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 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 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 #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 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 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 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
374fn 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 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 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 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 assert!(derive(r#"#[folder = "."] #[metadata_only = "yes"] struct Assets;"#).is_ok());
566 }
567}