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
59#[derive(Debug, Clone)]
60enum InnerDir {
61    Embed(include_dir::Dir<'static>, &'static str),
62    Path {
63        root: std::path::PathBuf,
64        path: std::path::PathBuf,
65    },
66}
67
68impl PartialEq for InnerDir {
69    fn eq(&self, other: &Self) -> bool {
70        self.path() == other.path()
71    }
72}
73
74impl Eq for InnerDir {}
75
76impl std::hash::Hash for InnerDir {
77    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
78        self.path().hash(state);
79    }
80}
81
82impl InnerDir {
83
84    fn into_dynamic(self) -> Self {
85        match &self {
86            InnerDir::Embed(dir, path) => 
87                Self::Path { root: PathBuf::from(path), path: PathBuf::from(path).join(dir.path()) },
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    /// This is a low-level API; prefer using higher-level methods for most use cases.
219    #[doc(hidden)]
220    pub fn entries(&self) -> Vec<DirEntry> {
221        match &self.inner {
222            InnerDir::Embed(dir, root) => dir
223                .files()
224                .map(|file| DirEntry {
225                    inner: InnerEntry::File(InnerFile::Embed(file.clone())),
226                })
227                .chain(dir.dirs().map(|subdir| DirEntry {
228                    inner: InnerEntry::Dir(InnerDir::Embed(subdir.clone(), root)),
229                }))
230                .collect(),
231            InnerDir::Path { root, path } => {
232                let mut entries = Vec::new();
233                if let Ok(entries_iter) = std::fs::read_dir(path) {
234                    for entry in entries_iter.flatten() {
235                        let entry_path = entry.path();
236                        if entry_path.is_file() {
237                            entries.push(DirEntry {
238                                inner: InnerEntry::File(InnerFile::Path {
239                                    root: root.clone(),
240                                    path: entry_path,
241                                }),
242                            });
243                        } else if entry_path.is_dir() {
244                            entries.push(DirEntry {
245                                inner: InnerEntry::Dir(InnerDir::Path {
246                                    root: root.clone(),
247                                    path: entry_path,
248                                }),
249                            });
250                        }
251                    }
252                }
253                entries
254            }
255        }
256    }
257
258    /// Returns the file with the given name if it exists in this directory.
259    /// The name is relative to the directory root.
260    pub fn get_file(&self, name: &str) -> Option<File> {
261        match &self.inner {
262            InnerDir::Embed(dir, _) => {
263                dir.get_file(dir.path().join(name)).map(|file| File {
264                    inner: InnerFile::Embed(file.clone()),
265                })
266            },
267            InnerDir::Path { root, path } => {
268                let new_path = path.join(name);
269                if new_path.is_file() {
270                    Some(File {
271                        inner: InnerFile::Path {
272                            root: root.clone(),
273                            path: new_path,
274                        },
275                    })
276                } else {
277                    None
278                }
279            }
280        }
281    }
282
283    /// Recursively walks all files in this directory and its subdirectories.
284    /// Returns an iterator over all files found.
285    pub fn walk(&self) -> impl Iterator<Item = File> {
286        let mut queue: VecDeque<DirEntry> = VecDeque::from_iter(self.entries().into_iter());
287        std::iter::from_fn(move || {
288            while let Some(entry) = queue.pop_front() {
289                match entry.inner {
290                    InnerEntry::File(file) => return Some(File { inner: file }),
291                    InnerEntry::Dir(dir) => queue.extend(Dir { inner: dir }.entries()),
292                }
293            }
294            None
295        })
296    }
297}
298
299#[derive(Debug, Clone, PartialEq, Eq, Hash)]
300/// Represents a file, which may be embedded or from the filesystem.
301/// Provides methods to access file contents and metadata.
302pub struct File {
303    inner: InnerFile,
304}
305
306impl File {
307    /// Returns the file name as a string slice, if available.
308    pub fn file_name(&self) -> Option<&str> {
309        self.path().file_name().and_then(|name| name.to_str())
310    }
311
312    /// Returns the file extension as a string slice, if available.
313    pub fn extension(&self) -> Option<&str> {
314        self.path().extension().and_then(|ext| ext.to_str())
315    }
316
317    /// Returns the absolute path of this file.
318    pub fn absolute_path(&self) -> &std::path::Path {
319        self.inner.absolute_path()
320    }
321
322    /// Returns true if this file is embedded in the binary.
323    pub fn is_embedded(&self) -> bool {
324        self.inner.is_embedded()
325    }
326
327    /// Returns the relative path of this file.
328    pub fn path(&self) -> &std::path::Path {
329        self.inner.path()
330    }
331
332    /// Reads the file contents as bytes.
333    pub fn read_bytes(&self) -> std::io::Result<Vec<u8>> {
334        match &self.inner {
335            InnerFile::Embed(file) => Ok(file.contents().to_vec()),
336            InnerFile::Path { path, .. } => std::fs::read(path),
337        }
338    }
339
340    /// Reads the file contents as a UTF-8 string.
341    /// Returns an error if the contents are not valid UTF-8.
342    pub fn read_str(&self) -> std::io::Result<String> {
343        match &self.inner {
344            InnerFile::Embed(file) => std::str::from_utf8(file.contents())
345                .map(str::to_owned)
346                .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e)),
347            InnerFile::Path { path, .. } => std::fs::read_to_string(path),
348        }
349    }
350
351    /// Returns the metadata for this file, such as modification time and size.
352    pub fn metadata(&self) -> std::io::Result<FileMetaData> {
353        match &self.inner {
354            InnerFile::Embed(file) => {
355                if let Some(metadata) = file.metadata() {
356                    Ok(FileMetaData {
357                        modified: metadata.modified(),
358                        size: file.contents().len() as u64,
359                    })
360                } else {
361                    Err(std::io::Error::new(
362                        std::io::ErrorKind::Other,
363                        "Failed to get embedded file metadata",
364                    ))
365                }
366            }
367            InnerFile::Path { path, .. } => {
368                let metadata = std::fs::metadata(path)?;
369                Ok(FileMetaData {
370                    modified: metadata.modified()?,
371                    size: metadata.len(),
372                })
373            }
374        }
375    }
376}
377
378#[derive(Debug, Clone, PartialEq, Eq, Hash)]
379/// Represents a directory entry, which may be a file or a directory.
380pub struct DirEntry {
381    inner: InnerEntry,
382}
383
384impl DirEntry {
385    /// Creates a directory entry from a file.
386    pub fn from_file(file: File) -> Self {
387        Self {
388            inner: InnerEntry::File(file.inner),
389        }
390    }
391
392    /// Creates a directory entry from a directory.
393    pub fn from_dir(dir: Dir) -> Self {
394        Self {
395            inner: InnerEntry::Dir(dir.inner),
396        }
397    }
398
399    /// Returns the relative path of this entry.
400    pub fn path(&self) -> &std::path::Path {
401        match &self.inner {
402            InnerEntry::File(file) => file.path(),
403            InnerEntry::Dir(dir) => dir.path(),
404        }
405    }
406
407    /// Returns the absolute path of this entry.
408    pub fn absolute_path(&self) -> &std::path::Path {
409        match &self.inner {
410            InnerEntry::File(file) => file.absolute_path(),
411            InnerEntry::Dir(dir) => dir.absolute_path(),
412        }
413    }
414
415    /// Returns true if this entry is embedded in the binary.
416    pub fn is_embedded(&self) -> bool {
417        matches!(&self.inner, InnerEntry::File(InnerFile::Embed(_)))
418            || matches!(&self.inner, InnerEntry::Dir(InnerDir::Embed(..)))
419    }
420
421    /// Returns true if this entry is a file.
422    pub const fn is_file(&self) -> bool {
423        matches!(&self.inner, InnerEntry::File(_))
424    }
425
426    /// Returns true if this entry is a directory.
427    pub const fn is_dir(&self) -> bool {
428        matches!(&self.inner, InnerEntry::Dir(_))
429    }
430
431    /// Converts this entry into a file, if it is a file.
432    pub fn into_file(self) -> Option<File> {
433        if let InnerEntry::File(file) = self.inner {
434            Some(File { inner: file })
435        } else {
436            None
437        }
438    }
439
440    /// Converts this entry into a directory, if it is a directory.
441    pub fn into_dir(self) -> Option<Dir> {
442        if let InnerEntry::Dir(dir) = self.inner {
443            Some(Dir { inner: dir })
444        } else {
445            None
446        }
447    }
448}
449
450#[derive(Debug, Clone, PartialEq, Eq, Hash)]
451/// Represents a set of root directories, supporting overlay and override semantics.
452/// Later directories in the set can override files from earlier ones with the same relative path.
453pub struct DirSet {
454    /// The list of root directories, in order of increasing precedence.
455    pub dirs: Vec<Dir>,
456}
457
458impl DirSet {
459    /// Creates a new DirSet from the given list of directories.
460    /// The order of directories determines override precedence.
461    pub fn new(dirs: Vec<Dir>) -> Self {
462        Self { dirs }
463    }
464
465    /// Returns all immediate entries from all root directories.
466    /// Entries from later roots do not override earlier ones in this list.
467    #[doc(hidden)]
468    pub fn entries(&self) -> Vec<DirEntry> {
469        self.dirs.iter().flat_map(|dir| dir.entries()).collect()
470    }
471
472    /// Returns the file with the given name, searching roots in reverse order.
473    /// Files in later roots override those in earlier roots if the relative path matches.
474    pub fn get_file(&self, name: &str) -> Option<File> {
475        for dir in self.dirs.iter().rev() {
476            if let Some(file) = dir.get_file(name) {
477                return Some(file);
478            }
479        }
480        None
481    }
482
483    /// Recursively walks all files in all root directories.
484    /// Files with the same relative path from different roots are all included.
485    pub fn walk(&self) -> impl Iterator<Item = File> {
486        let mut queue: VecDeque<DirEntry> = VecDeque::with_capacity(self.dirs.len() * 128); // Assuming an average of 128 entries per directory
487        for dir in self.dirs.iter() {
488            queue.push_back(DirEntry::from_dir(dir.clone()));
489        }
490        std::iter::from_fn(move || {
491            while let Some(entry) = queue.pop_front() {
492                match entry.inner {
493                    InnerEntry::File(file) => return Some(File { inner: file }),
494                    InnerEntry::Dir(dir) => {
495                        for child in( Dir { inner: dir }).entries().into_iter().rev() {
496                            queue.push_front(child);
497                        }
498                    },
499                }
500            }
501            None
502        })
503    }
504
505    /// Recursively walks all files, yielding only the highest-precedence file for each relative path.
506    /// This implements the override behaviour: later roots take precedence over earlier ones.
507    pub fn walk_override(&self) -> impl Iterator<Item = File> {
508        let mut history = std::collections::HashSet::new();
509        let mut queue: VecDeque<DirEntry> = VecDeque::with_capacity(self.dirs.len() * 128); // Assuming an average of 128 entries per directory
510        for dir in self.dirs.iter() {
511            queue.push_front(DirEntry::from_dir(dir.clone()));
512        }
513        std::iter::from_fn(move || {
514            while let Some(entry) = queue.pop_front() {
515                match entry.inner {
516                    InnerEntry::File(file) => {                        
517                        if  history.insert(file.path().to_owned()) {                           
518                            return Some(File { inner: file })
519                        }
520                    },
521                    InnerEntry::Dir(dir) => {
522                        for child in( Dir { inner: dir }).entries().into_iter() {
523                            queue.push_front(child);
524                        }
525                    },
526                }
527            }
528            None
529        })
530    }
531}