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 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 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 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 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 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 let canonical_file_path = file_path.canonicalize().ok()?;
218 if !canonical_file_path.starts_with(#canonical_folder_path) {
219 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 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 pub fn get(file_path: &str) -> ::std::option::Option<#crate_path::EmbeddedFile> {
251 None
253 }
254
255 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 #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
376fn 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 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 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#[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}