fs_embed/
lib.rs

1use std::{collections::VecDeque, path::PathBuf};
2
3pub use fs_embed_macros::fs_embed;
4
5pub struct FileMetaData {
6    /// The last modification time of the file.
7    pub modified: std::time::SystemTime,
8    /// The size of the file in bytes.
9    pub size: u64,
10}
11
12#[derive(Debug, Clone)]
13enum InnerFile {
14    Embed(include_dir::File<'static>),
15    Path {
16        root: std::path::PathBuf,
17        path: std::path::PathBuf,
18    },
19}
20
21impl PartialEq for InnerFile {
22    fn eq(&self, other: &Self) -> bool {
23        self.path() == other.path()
24    }
25}
26
27impl Eq for InnerFile {}
28
29impl std::hash::Hash for InnerFile {
30    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
31        self.path().hash(state);
32    }
33}
34
35impl InnerFile {
36    #[inline(always)]
37    fn absolute_path(&self) -> &std::path::Path {
38        match self {
39            InnerFile::Embed(file) => file.path(),
40            InnerFile::Path { path, .. } => path.as_path(),
41        }
42    }
43
44    #[inline(always)]
45    fn is_embedded(&self) -> bool {
46        matches!(self, InnerFile::Embed(_))
47    }
48
49    #[inline(always)]
50    pub fn path(&self) -> &std::path::Path {
51        match self {
52            InnerFile::Embed(dir) => dir.path(),
53            InnerFile::Path { root, path } => path.strip_prefix(root).unwrap_or(path),
54        }
55    }
56}
57
58#[derive(Debug, Clone)]
59enum InnerDir {
60    Embed(include_dir::Dir<'static>, &'static str),
61    Path {
62        root: std::path::PathBuf,
63        path: std::path::PathBuf,
64    },
65}
66
67impl PartialEq for InnerDir {
68    fn eq(&self, other: &Self) -> bool {
69        self.path() == other.path()
70    }
71}
72
73impl Eq for InnerDir {}
74
75impl std::hash::Hash for InnerDir {
76    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
77        self.path().hash(state);
78    }
79}
80
81impl InnerDir {
82    fn into_dynamic(self) -> Self {
83        match &self {
84            InnerDir::Embed(dir, path) => Self::Path {
85                root: PathBuf::from(path),
86                path: PathBuf::from(path).join(dir.path()),
87            },
88            InnerDir::Path { .. } => self,
89        }
90    }
91
92    #[inline(always)]
93    fn is_embedded(&self) -> bool {
94        matches!(self, InnerDir::Embed(..))
95    }
96
97    #[inline(always)]
98    fn path(&self) -> &std::path::Path {
99        match self {
100            InnerDir::Embed(dir, _) => dir.path(),
101            InnerDir::Path { root, path } => path.strip_prefix(root).unwrap_or(path),
102        }
103    }
104
105    #[inline(always)]
106    fn absolute_path(&self) -> &std::path::Path {
107        match self {
108            InnerDir::Embed(dir, _) => dir.path(),
109            InnerDir::Path { path, .. } => path.as_path(),
110        }
111    }
112}
113
114#[derive(Debug, Clone)]
115enum InnerEntry {
116    File(InnerFile),
117    Dir(InnerDir),
118}
119
120impl PartialEq for InnerEntry {
121    fn eq(&self, other: &Self) -> bool {
122        match (self, other) {
123            (InnerEntry::File(a), InnerEntry::File(b)) => a == b,
124            (InnerEntry::Dir(a), InnerEntry::Dir(b)) => a == b,
125            _ => false,
126        }
127    }
128}
129
130impl Eq for InnerEntry {}
131
132impl std::hash::Hash for InnerEntry {
133    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
134        match self {
135            InnerEntry::File(file) => {
136                0u8.hash(state); // Differentiate file from dir
137                file.hash(state)
138            }
139            InnerEntry::Dir(dir) => {
140                1u8.hash(state); // Differentiate dir from file
141                dir.hash(state)
142            }
143        }
144    }
145}
146
147#[derive(Debug, Clone, PartialEq, Eq, Hash)]
148/// Represents a directory, which may be embedded or from the filesystem.
149/// Provides methods to enumerate and access files and subdirectories.
150/// Represents a directory, which may be embedded or from the filesystem.
151/// Provides methods to enumerate and access files and subdirectories.
152pub struct Dir {
153    inner: InnerDir,
154}
155
156impl Dir {
157    /// Creates a directory from an embedded `include_dir::Dir` and its root path.
158    /// Intended for use in tests and advanced scenarios.
159    pub const fn from_embedded(dir: include_dir::Dir<'static>, path: &'static str) -> Self {
160        Self {
161            inner: InnerDir::Embed(dir, path),
162        }
163    }
164
165    /// Creates a new directory from the given path, relative to the manifest directory at build time.
166    /// The path can be any valid subdirectory or file path.
167    pub fn from_path(path: &std::path::Path) -> Self {
168        const BASE_DIR: &'static str = env!("CARGO_MANIFEST_DIR");
169        let base_path = std::path::PathBuf::from(BASE_DIR);
170        Self {
171            inner: InnerDir::Path {
172                root: base_path.join(path),
173                path: base_path.join(path),
174            },
175        }
176    }
177
178    /// Converts an embedded directory to a dynamic (filesystem-backed) directory if possible.
179    /// For embedded directories, this will create a Path variant using the embedded root path.
180    pub fn into_dynamic(self) -> Self {
181        Self {
182            inner: self.inner.into_dynamic(),
183        }
184    }
185
186    /// Automatically converts to a dynamic directory if in debug mode (cfg!(debug_assertions)).
187    /// In release mode, returns self unchanged.
188    pub fn auto_dynamic(self) -> Self {
189        if cfg!(debug_assertions) {
190            return self.into_dynamic();
191        } else {
192            return self;
193        }
194    }
195
196    /// Creates a new root directory from the given string path, relative to the manifest directory.
197    /// The path must be a string literal or static string.
198    pub fn from_str(path: &'static str) -> Self {
199        Self::from_path(std::path::Path::new(path))
200    }
201
202    /// Returns true if this directory is embedded in the binary.
203    pub fn is_embedded(&self) -> bool {
204        self.inner.is_embedded()
205    }
206
207    /// Returns the relative path of this directory.
208    pub fn path(&self) -> &std::path::Path {
209        self.inner.path()
210    }
211
212    /// Returns the absolute path of this directory.
213    pub fn absolute_path(&self) -> &std::path::Path {
214        self.inner.absolute_path()
215    }
216
217    /// Returns all immediate entries (files and subdirectories) in this directory.
218    pub fn entries(&self) -> Vec<DirEntry> {
219        match &self.inner {
220            InnerDir::Embed(dir, root) => dir
221                .files()
222                .map(|file| DirEntry {
223                    inner: InnerEntry::File(InnerFile::Embed(file.clone())),
224                })
225                .chain(dir.dirs().map(|subdir| DirEntry {
226                    inner: InnerEntry::Dir(InnerDir::Embed(subdir.clone(), root)),
227                }))
228                .collect(),
229            InnerDir::Path { root, path } => {
230                let mut entries = Vec::new();
231                if let Ok(entries_iter) = std::fs::read_dir(path) {
232                    for entry in entries_iter.flatten() {
233                        let entry_path = entry.path();
234                        if entry_path.is_file() {
235                            entries.push(DirEntry {
236                                inner: InnerEntry::File(InnerFile::Path {
237                                    root: root.clone(),
238                                    path: entry_path,
239                                }),
240                            });
241                        } else if entry_path.is_dir() {
242                            entries.push(DirEntry {
243                                inner: InnerEntry::Dir(InnerDir::Path {
244                                    root: root.clone(),
245                                    path: entry_path,
246                                }),
247                            });
248                        }
249                    }
250                }
251                entries
252            }
253        }
254    }
255
256    /// Returns the file with the given name if it exists in this directory.
257    /// The name is relative to the directory root.
258    pub fn get_file(&self, name: &str) -> Option<File> {
259        match &self.inner {
260            InnerDir::Embed(dir, _) => dir.get_file(dir.path().join(name)).map(|file| File {
261                inner: InnerFile::Embed(file.clone()),
262            }),
263            InnerDir::Path { root, path } => {
264                let new_path = path.join(name);
265                if new_path.is_file() {
266                    Some(File {
267                        inner: InnerFile::Path {
268                            root: root.clone(),
269                            path: new_path,
270                        },
271                    })
272                } else {
273                    None
274                }
275            }
276        }
277    }
278
279    /// Returns a reference to the directory with the given name, if it exists.
280    pub fn get_dir(&self, name: &str) -> Option<Dir> {
281        match &self.inner {
282            InnerDir::Embed(dir, root) => dir.get_dir(dir.path().join(name)).map(|subdir| Dir {
283                inner: InnerDir::Embed(subdir.clone(), root),
284            }),
285            InnerDir::Path { root, path } => {
286                let new_path = path.join(name);
287                if new_path.is_dir() {
288                    Some(Dir {
289                        inner: InnerDir::Path {
290                            root: root.clone(),
291                            path: new_path,
292                        },
293                    })
294                } else {
295                    None
296                }
297            }
298        }
299    }
300
301    /// Recursively walks all files in this directory and its subdirectories.
302    /// Returns an iterator over all files found.
303    pub fn walk(&self) -> impl Iterator<Item = File> {
304        let mut queue: VecDeque<DirEntry> = VecDeque::from_iter(self.entries().into_iter());
305        std::iter::from_fn(move || {
306            while let Some(entry) = queue.pop_front() {
307                match entry.inner {
308                    InnerEntry::File(file) => return Some(File { inner: file }),
309                    InnerEntry::Dir(dir) => queue.extend(Dir { inner: dir }.entries()),
310                }
311            }
312            None
313        })
314    }
315}
316
317#[derive(Debug, Clone, PartialEq, Eq, Hash)]
318/// Represents a file, which may be embedded or from the filesystem.
319/// Provides methods to access file contents and metadata.
320pub struct File {
321    inner: InnerFile,
322}
323
324impl File {
325    /// Returns the file name as a string slice, if available.
326    pub fn file_name(&self) -> Option<&str> {
327        self.path().file_name().and_then(|name| name.to_str())
328    }
329
330    /// Returns the file extension as a string slice, if available.
331    pub fn extension(&self) -> Option<&str> {
332        self.path().extension().and_then(|ext| ext.to_str())
333    }
334
335    /// Returns the absolute path of this file.
336    pub fn absolute_path(&self) -> &std::path::Path {
337        self.inner.absolute_path()
338    }
339
340    /// Returns true if this file is embedded in the binary.
341    pub fn is_embedded(&self) -> bool {
342        self.inner.is_embedded()
343    }
344
345    /// Returns the relative path of this file.
346    pub fn path(&self) -> &std::path::Path {
347        self.inner.path()
348    }
349
350    /// Reads the file contents as bytes.
351    pub fn read_bytes(&self) -> std::io::Result<Vec<u8>> {
352        match &self.inner {
353            InnerFile::Embed(file) => Ok(file.contents().to_vec()),
354            InnerFile::Path { path, .. } => std::fs::read(path),
355        }
356    }
357
358    /// Reads the file contents as a UTF-8 string.
359    /// Returns an error if the contents are not valid UTF-8.
360    pub fn read_str(&self) -> std::io::Result<String> {
361        match &self.inner {
362            InnerFile::Embed(file) => std::str::from_utf8(file.contents())
363                .map(str::to_owned)
364                .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e)),
365            InnerFile::Path { path, .. } => std::fs::read_to_string(path),
366        }
367    }
368
369    /// Returns the metadata for this file, such as modification time and size.
370    pub fn metadata(&self) -> std::io::Result<FileMetaData> {
371        match &self.inner {
372            InnerFile::Embed(file) => {
373                if let Some(metadata) = file.metadata() {
374                    Ok(FileMetaData {
375                        modified: metadata.modified(),
376                        size: file.contents().len() as u64,
377                    })
378                } else {
379                    Err(std::io::Error::new(
380                        std::io::ErrorKind::Other,
381                        "Failed to get embedded file metadata",
382                    ))
383                }
384            }
385            InnerFile::Path { path, .. } => {
386                let metadata = std::fs::metadata(path)?;
387                Ok(FileMetaData {
388                    modified: metadata.modified()?,
389                    size: metadata.len(),
390                })
391            }
392        }
393    }
394}
395
396#[derive(Debug, Clone, PartialEq, Eq, Hash)]
397/// Represents a directory entry, which may be a file or a directory.
398pub struct DirEntry {
399    inner: InnerEntry,
400}
401
402impl DirEntry {
403    /// Creates a directory entry from a file.
404    pub fn from_file(file: File) -> Self {
405        Self {
406            inner: InnerEntry::File(file.inner),
407        }
408    }
409
410    /// Creates a directory entry from a directory.
411    pub fn from_dir(dir: Dir) -> Self {
412        Self {
413            inner: InnerEntry::Dir(dir.inner),
414        }
415    }
416
417    /// Returns the relative path of this entry.
418    pub fn path(&self) -> &std::path::Path {
419        match &self.inner {
420            InnerEntry::File(file) => file.path(),
421            InnerEntry::Dir(dir) => dir.path(),
422        }
423    }
424
425    /// Returns the absolute path of this entry.
426    pub fn absolute_path(&self) -> &std::path::Path {
427        match &self.inner {
428            InnerEntry::File(file) => file.absolute_path(),
429            InnerEntry::Dir(dir) => dir.absolute_path(),
430        }
431    }
432
433    /// Returns true if this entry is embedded in the binary.
434    pub fn is_embedded(&self) -> bool {
435        matches!(&self.inner, InnerEntry::File(InnerFile::Embed(_)))
436            || matches!(&self.inner, InnerEntry::Dir(InnerDir::Embed(..)))
437    }
438
439    /// Returns true if this entry is a file.
440    pub const fn is_file(&self) -> bool {
441        matches!(&self.inner, InnerEntry::File(_))
442    }
443
444    /// Returns true if this entry is a directory.
445    pub const fn is_dir(&self) -> bool {
446        matches!(&self.inner, InnerEntry::Dir(_))
447    }
448
449    /// Converts this entry into a file, if it is a file.
450    pub fn into_file(self) -> Option<File> {
451        if let InnerEntry::File(file) = self.inner {
452            Some(File { inner: file })
453        } else {
454            None
455        }
456    }
457
458    /// Converts this entry into a directory, if it is a directory.
459    pub fn into_dir(self) -> Option<Dir> {
460        if let InnerEntry::Dir(dir) = self.inner {
461            Some(Dir { inner: dir })
462        } else {
463            None
464        }
465    }
466}
467
468#[derive(Debug, Clone, PartialEq, Eq, Hash)]
469/// Represents a set of root directories, supporting overlay and override semantics.
470/// Later directories in the set can override files from earlier ones with the same relative path.
471pub struct DirSet {
472    /// The list of root directories, in order of increasing precedence.
473    pub dirs: Vec<Dir>,
474}
475
476impl DirSet {
477    /// Creates a new DirSet from the given list of directories.
478    /// The order of directories determines override precedence.
479    pub fn new(dirs: Vec<Dir>) -> Self {
480        Self { dirs }
481    }
482
483    /// Returns all immediate entries from all root directories.
484    /// Entries from later roots do not override earlier ones in this list.
485    #[doc(hidden)]
486    pub fn entries(&self) -> Vec<DirEntry> {
487        self.dirs.iter().flat_map(|dir| dir.entries()).collect()
488    }
489
490    /// Returns the file with the given name, searching roots in reverse order.
491    /// Files in later roots override those in earlier roots if the relative path matches.
492    pub fn get_file(&self, name: &str) -> Option<File> {
493        for dir in self.dirs.iter().rev() {
494            if let Some(file) = dir.get_file(name) {
495                return Some(file);
496            }
497        }
498        None
499    }
500
501    pub fn get_dir(&self, name: &str) -> Option<Dir> {
502        for dir in self.dirs.iter().rev() {
503            if let Some(subdir) = dir.get_dir(name) {
504                return Some(subdir);
505            }
506        }
507        None
508    }
509
510    /// Recursively walks all files in all root directories.
511    /// Files with the same relative path from different roots are all included.
512    pub fn walk(&self) -> impl Iterator<Item = File> {
513        let mut queue: Vec<DirEntry> = Vec::with_capacity(self.dirs.len() * 128); // Assuming an average of 128 entries per directory
514        for dir in self.dirs.iter() {
515            queue.push(DirEntry::from_dir(dir.clone()));
516        }
517        std::iter::from_fn(move || {
518            while let Some(entry) = queue.pop() {
519                match entry.inner {
520                    InnerEntry::File(file) => return Some(File { inner: file }),
521                    InnerEntry::Dir(dir) => {
522                        for child in (Dir { inner: dir }).entries().into_iter().rev() {
523                            queue.push(child);
524                        }
525                    }
526                }
527            }
528            None
529        })
530    }
531
532    /// Recursively walks all files, yielding only the highest-precedence file for each relative path.
533    /// This implements the override behaviour: later roots take precedence over earlier ones.
534    pub fn walk_override(&self) -> impl Iterator<Item = File> {
535        let mut history = std::collections::HashSet::new();
536        let mut stack: Vec<DirEntry> = Vec::with_capacity(self.dirs.len() * 128); // DFS uses stack
537        for dir in self.dirs.iter() {
538            stack.push(DirEntry::from_dir(dir.clone()));
539        }
540        std::iter::from_fn(move || {
541            while let Some(entry) = stack.pop() {
542                match entry.inner {
543                    InnerEntry::File(file) => {
544                        if history.insert(file.path().to_owned()) {
545                            return Some(File { inner: file });
546                        }
547                    }
548                    InnerEntry::Dir(dir) => {
549                        // Push children in reverse order to preserve order in DFS
550                        let children = Dir { inner: dir }.entries();
551                        for child in children.into_iter() {
552                            stack.push(child);
553                        }
554                    }
555                }
556            }
557            None
558        })
559    }
560}