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    Dynamic(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::Dynamic(dyn_file) => Ok(FileReader::Dynamic(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::Dynamic(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::Dynamic(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/// Get a dynamic file by its relative path. Returns None if not found or not a file.
177fn get_file_for_root(root: &str, path: &str) -> Option<DynFile> {
178    let pathbuff = Path::new(&*root).join(path);
179    if pathbuff.is_file() {            
180        Some(DynFile::new(Arc::from(pathbuff.to_str()?), Arc::from(path)))
181    } else {
182        None
183    }
184}
185
186/// Iterate over all files in the dynamic silo.
187fn iter_root(root: &str) -> impl Iterator<Item = File> {
188    let root_path = PathBuf::from(&*root);
189    walkdir::WalkDir::new(&root_path)
190        .into_iter()
191        .filter_map(move |entry| {
192            let entry = entry.ok()?;
193            if entry.file_type().is_file() {
194                let relative_path = entry.path().strip_prefix(&root_path).ok()?;
195                Some(File {
196                    inner: FileKind::Dynamic(DynFile::new(
197                        Arc::from(entry.path().to_str()?),
198                        Arc::from(relative_path.to_str()?),
199                    )),
200                })
201            } else {
202                None
203            }
204        })
205}
206
207/// Represents a set of dynamic (filesystem) files rooted at a directory.
208#[derive(Debug, Clone)]
209struct DynamicSilo {
210    root: Arc<str>,
211}
212
213impl DynamicSilo {
214    /// Creates a new DynamicSilo from a dynamic root path.
215    /// The root path must be valid UTF-8.
216    pub fn new(root: &str) -> Self {
217        Self { root: Arc::from(root) }
218    }
219
220    /// Gets a dynamic file by its relative path.
221    /// Returns `None` if the file is not found or is not a valid file.
222    pub fn get_file(&self, path: &str) -> Option<DynFile> {
223        get_file_for_root(self.root.as_ref(), path)
224    }
225
226    /// Iterates over all files in the dynamic silo.
227    /// Returns an iterator of `File` objects representing the files.
228    pub fn iter(&self) -> impl Iterator<Item = File> {
229        iter_root(self.root.as_ref())
230    }
231}
232
233/// Represents a set of static (filesystem) files rooted at a directory.
234/// Static silos are backed by a fixed directory path.
235#[derive(Debug, Clone)]
236struct StaticSilo {
237    root: &'static str,
238}
239
240impl StaticSilo {
241    /// Creates a new StaticSilo from a static root path.
242    pub const fn new(root: &'static str) -> Self {
243        Self { root }
244    }
245
246    /// Gets a static file by its relative path.
247    /// Returns `None` if the file is not found or is not a valid file.
248    pub fn get_file(&self, path: &str) -> Option<DynFile> {
249        get_file_for_root(self.root, path)
250    }
251
252    /// Iterates over all files in the static silo.
253    /// Returns an iterator of `File` objects representing the files.
254    pub fn iter(&self) -> impl Iterator<Item = File> {
255        iter_root(self.root)
256    }
257}
258
259/// Internal enum for silo variants (embedded or dynamic).
260#[derive(Debug, Clone)]
261enum InnerSilo {
262    Embed(EmbedSilo),
263    Static(StaticSilo),
264    Dynamic(DynamicSilo),
265}
266
267/// Represents a root directory, which may be embedded or dynamic.
268#[derive(Debug, Clone)]
269pub struct Silo {
270    inner: InnerSilo,
271}
272
273impl Silo {
274
275    #[doc(hidden)]
276    /// Creates a Silo from an embedded PHF map and root path.
277    pub const fn from_embedded(phf_map: &'static phf::Map<&'static str, EmbedEntry>, root: &'static str) -> Self {
278        Self {
279            inner: InnerSilo::Embed(EmbedSilo::new(phf_map, root)),
280        }
281    }
282
283    #[doc(hidden)]
284    /// Creates a Silo from a static path (dynamic root).
285    pub const fn from_static(path: &'static str) -> Self {
286        Self {
287            inner: InnerSilo::Static(StaticSilo::new(path)),
288        }
289    }
290
291    /// Creates a Silo from a dynamic path (dynamic root).
292    pub fn new(path: &str) -> Self {
293        Self {
294            inner: InnerSilo::Dynamic(DynamicSilo::new(path)),
295        }
296    }
297
298    /// Converts the Silo to a dynamic Silo if it is currently embedded.
299    /// Returns `self` unchanged if the Silo is already dynamic or static.
300    pub fn into_dynamic(self) -> Self {
301        match self.inner {
302            InnerSilo::Embed(emb_silo) => Self::from_static(&*emb_silo.root),
303            InnerSilo::Static(_) => self,
304            InnerSilo::Dynamic(_) => self,
305        }
306    }
307
308    /// Automatically converts the Silo to a dynamic directory in debug mode (`cfg!(debug_assertions)`).
309    /// In release mode, returns `self` unchanged.
310    /// This is a no-op if the Silo is not embedded.
311    pub fn auto_dynamic(self) -> Self {
312        if cfg!(debug_assertions) {
313            self.into_dynamic()
314        } else {
315            self
316        }
317    }
318
319    /// Returns `true` if this Silo is dynamic (filesystem-backed).
320    pub fn is_dynamic(&self) -> bool {
321        matches!(self.inner, InnerSilo::Static(_) | InnerSilo::Dynamic(_))
322    }
323
324    /// Returns `true` if this Silo is embedded in the binary.
325    pub fn is_embedded(&self) -> bool {
326        matches!(self.inner, InnerSilo::Embed(_))
327    }
328
329    /// Gets a file by its relative path from this Silo.
330    /// Returns `None` if the file is not found.
331    pub fn get_file(&self, path: &str) -> Option<File> {
332        match &self.inner {
333            InnerSilo::Embed(embed) => embed.get_file(path).map(|f| File {
334                inner: FileKind::Embed(f),
335            }),
336            InnerSilo::Static(dyn_silo) => dyn_silo.get_file(path).map(|f| File {
337                inner: FileKind::Dynamic(f),
338            }),
339            InnerSilo::Dynamic(dyn_silo) => dyn_silo.get_file(path).map(|f| File {
340                inner: FileKind::Dynamic(f),
341            }),
342        }
343    }
344
345    /// Iterates over all files in this Silo.
346    /// Returns a boxed iterator of `File` objects representing the files.
347    pub fn iter(&self) -> Box<dyn Iterator<Item = File> + '_> {
348        match &self.inner {
349            InnerSilo::Embed(embd) => Box::new(embd.iter()),
350            InnerSilo::Static(dynm) => Box::new(dynm.iter()),
351            InnerSilo::Dynamic(dynm) => Box::new(dynm.iter()),
352        }
353    }
354}
355
356
357
358/// Represents a set of root directories, supporting overlay and override semantics.
359/// Later directories in the set can override files from earlier ones with the same relative path.
360#[derive(Debug, Clone)]
361pub struct SiloSet {
362    /// The list of root directories, in order of increasing precedence.
363    pub silos: Vec<Silo>,
364}
365
366impl SiloSet {
367    /// Creates a new SiloSet from the given list of directories.
368    /// The order of directories determines override precedence.
369    /// Create a new SiloSet from a list of Silos. Order determines override precedence.
370    pub fn new(dirs: Vec<Silo>) -> Self {
371        Self { silos: dirs }
372    }
373
374
375    /// Returns the file with the given name, searching roots in reverse order.
376    /// Files in later roots override those in earlier roots if the relative path matches.
377    /// Get a file by name, searching Silos in reverse order (highest precedence first).
378    pub fn get_file(&self, name: &str) -> Option<File> {
379        for silo in self.silos.iter().rev() {
380            if let Some(file) = silo.get_file(name) {
381                return Some(file);
382            }
383        }
384        None
385    }
386
387    /// Recursively walks all files in all root directories.
388    /// Files with the same relative path from different roots are all included.
389    /// Iterate all files in all Silos, including duplicates.
390    pub fn iter(&self) -> impl Iterator<Item = File> + '_ {
391        self.silos.iter().rev().flat_map(|silo| silo.iter())
392    }
393
394    /// Recursively walks all files, yielding only the highest-precedence file for each relative path.
395    /// This implements the override behaviour: later roots take precedence over earlier ones.
396    /// Iterate all files, yielding only the highest-precedence file for each path.
397    pub fn iter_override(&self) -> impl Iterator<Item = File> + '_ {
398        let mut history = std::collections::HashSet::new();
399        self.iter().filter(move |file| history.insert(file.clone()) )
400    }
401}
402
403
404/// Reader for file contents, either embedded or dynamic.
405pub enum FileReader {
406    Embed(std::io::Cursor<&'static [u8]>),
407    Dynamic(std::fs::File),
408}
409
410/// Implements std::io::Read for FileReader.
411impl std::io::Read for FileReader {
412    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
413        match self {
414            FileReader::Embed(c) => c.read(buf),
415            FileReader::Dynamic(f) => f.read(buf),
416        }
417    }
418}