rust_silos/
lib.rs

1// Re-export phf_map macro for consumers of rust-silos
2pub use phf::phf_map;
3pub use phf;
4use std::hash::Hash;
5use std::io::Cursor;
6use std::path::{Path, PathBuf};
7use std::sync::Arc;
8use thiserror::Error;
9
10
11/// Error type for file and silo operations.
12#[derive(Debug, Error)]
13pub enum Error {
14    #[error("Failed to decode file contents: {source}")]
15    DecodeError {
16        #[from]
17        source: std::string::FromUtf8Error,
18    },
19    #[error("File not found")]
20    NotFound,
21    #[error("I/O error: {source}")]
22    IoError {
23        #[from]
24        source: std::io::Error,
25    },
26}
27
28
29/// Metadata and contents for an embedded file.
30#[derive(Debug)]
31pub struct EmbedEntry {
32    pub path: &'static str,
33    pub contents: &'static [u8],
34    pub size: usize,
35    pub modified: u64,
36}
37
38/// Handle to an embedded file entry.
39#[derive(Copy, Clone, Debug)]
40struct EmbedFile {
41    inner: &'static EmbedEntry,
42}
43
44impl EmbedFile {
45    /// Returns the relative path of the embedded file.
46    pub fn path(&self) -> &Path {
47        Path::new(self.inner.path)
48    }
49}
50
51/// Internal enum for file variants (embedded or dynamic).
52#[derive(Debug, Clone)]
53enum FileKind {
54    Embed(EmbedFile),
55    Dyn(DynFile),
56}
57
58/// Represents a file, which may be embedded or dynamic.
59#[derive(Debug, Clone)]
60pub struct File {
61    inner: FileKind,
62}
63
64impl File {
65    /// Returns a reader for the file's contents. May return an error if the file cannot be opened.
66    pub fn reader(&self) -> Result<FileReader, Error> {
67        match &self.inner {
68            FileKind::Embed(embed) => Ok(FileReader::Embed(Cursor::new(embed.inner.contents))),
69            FileKind::Dyn(dyn_file) => Ok(FileReader::Dyn(std::fs::File::open(
70                dyn_file.absolute_path(),
71            )?)),
72        }
73    }
74
75    /// Returns the relative path of the file.
76    pub fn path(&self) -> &Path {
77        match &self.inner {
78            FileKind::Embed(embed) => embed.path(),
79            FileKind::Dyn(dyn_file) => dyn_file.path(),
80        }
81    }
82
83    /// Returns true if the file is embedded in the binary.
84    pub fn is_embedded(&self) -> bool {
85        matches!(self.inner, FileKind::Embed(_))
86    }
87
88    /// Returns the absolute path if the file is dynamic, or None if embedded.
89    pub fn absolute_path(&self) -> Option<&Path> {
90        match &self.inner {
91            FileKind::Embed(_) => None,
92            FileKind::Dyn(dyn_file) => Some(dyn_file.absolute_path()),
93        }
94    }
95
96    /// Returns the file extension, if any.
97    pub fn extension(&self) -> Option<&str> {
98        self.path().extension().and_then(|s| s.to_str())
99    }
100}
101
102/// Files are equal if their relative paths are equal.
103impl PartialEq for File {
104    fn eq(&self, other: &Self) -> bool {
105        self.path() == other.path()
106    }
107}
108
109/// Hashes a file by its relative path.
110impl Hash for File {
111    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
112        self.path().hash(state);
113    }
114}
115
116impl Eq for File {}
117
118
119
120/// Represents a set of embedded files and their root.
121#[derive(Debug, Clone)]
122struct EmbedSilo {
123    map: &'static phf::Map<&'static str, EmbedEntry>,
124    root: &'static str,
125}
126
127impl EmbedSilo {
128    /// Create a new EmbedSilo from a PHF map and root path.
129    pub const fn new(map: &'static phf::Map<&'static str, EmbedEntry>, root: &'static str) -> Self {
130        Self { map, root }
131    }
132
133    /// Get an embedded file by its relative path.
134    /// Returns None if not found.
135    pub fn get_file(&self, path: &str) -> Option<EmbedFile> {
136        self.map.get(path).map(|entry| EmbedFile { inner: entry })
137    }
138
139    /// Iterate over all embedded files in this silo.
140    pub fn iter(&self) -> impl Iterator<Item = File> + '_ {
141        self.map.values().map(|entry| File {
142            inner: FileKind::Embed(EmbedFile { inner: entry }),
143        })
144    }
145}
146
147/// Represents a file from the filesystem (not embedded).
148#[derive(Debug, Clone)]
149struct DynFile {
150    rel_path: Arc<str>,
151    full_path: Arc<str>,
152}
153
154impl DynFile {
155    /// root is the base directory where the file is located, and path is the relative path to the file.
156    /// Create a new DynFile from absolute and relative paths.
157    /// Both must be valid UTF-8.
158    pub fn new<S: AsRef<str>>(full_path: S, rel_path: S) -> Self {
159        Self {
160            rel_path: Arc::from(rel_path.as_ref()),
161            full_path: Arc::from(full_path.as_ref()),
162        }
163    }
164
165    /// Returns the relative path of the file.
166    pub fn path(&self) -> &Path {
167        Path::new(&*self.rel_path)
168    }
169
170    /// Returns the absolute path of the file.
171    pub fn absolute_path(&self) -> &Path {
172        Path::new(&*self.full_path)
173    }
174}
175
176/// Represents a set of dynamic (filesystem) files rooted at a directory.
177#[derive(Debug, Clone)]
178struct DynSilo {
179    root: &'static str,
180}
181
182
183impl DynSilo {
184    /// Create a new DynSilo from a static root path.
185    pub const fn new(root: &'static str) -> Self {
186        Self { root }
187    }
188
189    /// Get a dynamic file by its relative path. Returns None if not found or not a file.
190    pub fn get_file(&self, path: &str) -> Option<DynFile> {
191        let pathbuff = Path::new(&*self.root).join(path);
192        if pathbuff.is_file() {            
193            Some(DynFile::new(Arc::from(pathbuff.to_str()?), Arc::from(path)))
194        } else {
195            None
196        }
197    }
198
199    /// Iterate over all files in the dynamic silo.
200    pub fn iter(&self) -> impl Iterator<Item = File> {
201        let root_path = PathBuf::from(&*self.root);
202        walkdir::WalkDir::new(&root_path)
203            .into_iter()
204            .filter_map(move |entry| {
205                let entry = entry.ok()?;
206                if entry.file_type().is_file() {
207                    let relative_path = entry.path().strip_prefix(&root_path).ok()?;
208                    Some(File {
209                        inner: FileKind::Dyn(DynFile::new(
210                            Arc::from(entry.path().to_str()?),
211                            Arc::from(relative_path.to_str()?),
212                        )),
213                    })
214                } else {
215                    None
216                }
217            })
218    }
219}
220
221/// Internal enum for silo variants (embedded or dynamic).
222#[derive(Debug, Clone)]
223enum InnerSilo {
224    Embed(EmbedSilo),
225    Dyn(DynSilo),
226}
227
228/// Represents a root directory, which may be embedded or dynamic.
229#[derive(Debug, Clone)]
230pub struct Silo {
231    inner: InnerSilo,
232}
233
234impl Silo {
235
236    /// Create a Silo from an EmbedSilo.
237    pub const fn from_embedded(phf_map: &'static phf::Map<&'static str, EmbedEntry>, root: &'static str) -> Self {
238        Self {
239            inner: InnerSilo::Embed(EmbedSilo::new(phf_map, root)),
240        }
241    }
242
243    /// Create a Silo from a static path (dynamic root).
244    pub const fn from_path(path: &'static str) -> Self {
245        Self {
246            inner: InnerSilo::Dyn(DynSilo::new(path)),
247        }
248    }
249
250    /// Convert to a dynamic Silo if currently embedded, otherwise returns self.
251    pub fn into_dynamic(self) -> Self {
252        match self.inner {
253            InnerSilo::Embed(emb_silo) => Self::from_path(&*emb_silo.root),
254            InnerSilo::Dyn(_) => self,
255        }
256    }
257
258    /// Automatically converts to a dynamic directory if in debug mode (cfg!(debug_assertions)).
259    /// In release mode, returns self unchanged.
260    /// Convert to a dynamic Silo in debug mode, otherwise returns self.
261    pub fn auto_dynamic(self) -> Self {
262        if cfg!(debug_assertions) {
263            return self.into_dynamic();
264        } else {
265            return self;
266        }
267    }
268
269    /// Returns true if this Silo is dynamic (filesystem-backed).
270    pub fn is_dynamic(&self) -> bool {
271        matches!(self.inner, InnerSilo::Dyn(_))
272    }
273
274    /// Returns true if this Silo is embedded in the binary.
275    pub fn is_embedded(&self) -> bool {
276        matches!(self.inner, InnerSilo::Embed(_))
277    }
278
279    /// Get a file by relative path from this Silo. Returns None if not found.
280    pub fn get_file(&self, path: &str) -> Option<File> {
281        match &self.inner {
282            InnerSilo::Embed(embed) => embed.get_file(path).map(|f| File {
283                inner: FileKind::Embed(f),
284            }),
285            InnerSilo::Dyn(dyn_silo) => dyn_silo.get_file(path).map(|f| File {
286                inner: FileKind::Dyn(f),
287            }),
288        }
289    }
290
291    /// Iterate over all files in this Silo.
292    pub fn iter(&self) -> Box<dyn Iterator<Item = File> + '_> {
293        match &self.inner {
294            InnerSilo::Embed(embd) => Box::new(embd.iter()),
295            InnerSilo::Dyn(dynm) => Box::new(dynm.iter()),
296        }
297    }
298    
299}
300
301
302
303/// Represents a set of root directories, supporting overlay and override semantics.
304/// Later directories in the set can override files from earlier ones with the same relative path.
305#[derive(Debug, Clone)]
306pub struct SiloSet {
307    /// The list of root directories, in order of increasing precedence.
308    pub silos: Vec<Silo>,
309}
310
311impl SiloSet {
312    /// Creates a new SiloSet from the given list of directories.
313    /// The order of directories determines override precedence.
314    /// Create a new SiloSet from a list of Silos. Order determines override precedence.
315    pub fn new(dirs: Vec<Silo>) -> Self {
316        Self { silos: dirs }
317    }
318
319
320    /// Returns the file with the given name, searching roots in reverse order.
321    /// Files in later roots override those in earlier roots if the relative path matches.
322    /// Get a file by name, searching Silos in reverse order (highest precedence first).
323    pub fn get_file(&self, name: &str) -> Option<File> {
324        for silo in self.silos.iter().rev() {
325            if let Some(file) = silo.get_file(name) {
326                return Some(file);
327            }
328        }
329        None
330    }
331
332    /// Recursively walks all files in all root directories.
333    /// Files with the same relative path from different roots are all included.
334    /// Iterate all files in all Silos, including duplicates.
335    pub fn iter(&self) -> impl Iterator<Item = File> + '_ {
336        self.silos.iter().rev().flat_map(|silo| silo.iter())
337    }
338
339    /// Recursively walks all files, yielding only the highest-precedence file for each relative path.
340    /// This implements the override behaviour: later roots take precedence over earlier ones.
341    /// Iterate all files, yielding only the highest-precedence file for each path.
342    pub fn iter_override(&self) -> impl Iterator<Item = File> + '_ {
343        let mut history = std::collections::HashSet::new();
344        self.iter().filter(move |file| history.insert(file.clone()) )
345    }
346}
347
348
349/// Reader for file contents, either embedded or dynamic.
350pub enum FileReader {
351    Embed(std::io::Cursor<&'static [u8]>),
352    Dyn(std::fs::File),
353}
354
355/// Implements std::io::Read for FileReader.
356impl std::io::Read for FileReader {
357    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
358        match self {
359            FileReader::Embed(c) => c.read(buf),
360            FileReader::Dyn(f) => f.read(buf),
361        }
362    }
363}