1use std::hash::Hash;
2use std::io::Cursor;
3use std::path::{Path, PathBuf};
4use std::sync::Arc;
5use thiserror::Error;
6
7pub use rust_silos_macros::embed_silo;
8
9
10#[derive(Debug, Error)]
12pub enum Error {
13 #[error("Failed to decode file contents: {source}")]
14 DecodeError {
15 #[from]
16 source: std::string::FromUtf8Error,
17 },
18 #[error("File not found")]
19 NotFound,
20 #[error("I/O error: {source}")]
21 IoError {
22 #[from]
23 source: std::io::Error,
24 },
25}
26
27
28#[derive(Debug)]
30pub struct EmbedEntry {
31 pub path: &'static str,
32 pub contents: &'static [u8],
33 pub size: usize,
34 pub modified: u64,
35}
36
37#[derive(Debug, Copy, Clone, Eq, PartialEq)]
39pub struct FileMeta {
40 pub size: usize,
41 pub modified: u64,
43}
44
45#[derive(Copy, Clone, Debug)]
47struct EmbedFile {
48 inner: &'static EmbedEntry,
49}
50
51impl EmbedFile {
52 pub fn path(&self) -> &Path {
54 Path::new(self.inner.path)
55 }
56}
57
58#[derive(Debug, Clone)]
60enum FileKind {
61 Embed(EmbedFile),
62 Dynamic(DynFile),
63}
64
65#[derive(Debug, Clone)]
67pub struct File {
68 inner: FileKind,
69}
70
71impl File {
72 pub fn reader(&self) -> Result<FileReader, Error> {
74 match &self.inner {
75 FileKind::Embed(embed) => Ok(FileReader::Embed(Cursor::new(embed.inner.contents))),
76 FileKind::Dynamic(dyn_file) => Ok(FileReader::Dynamic(std::fs::File::open(
77 dyn_file.absolute_path(),
78 )?)),
79 }
80 }
81
82 pub fn path(&self) -> &Path {
84 match &self.inner {
85 FileKind::Embed(embed) => embed.path(),
86 FileKind::Dynamic(dyn_file) => dyn_file.path(),
87 }
88 }
89
90 pub fn is_embedded(&self) -> bool {
92 matches!(self.inner, FileKind::Embed(_))
93 }
94
95 pub fn absolute_path(&self) -> Option<&Path> {
97 match &self.inner {
98 FileKind::Embed(_) => None,
99 FileKind::Dynamic(dyn_file) => Some(dyn_file.absolute_path()),
100 }
101 }
102
103 pub fn extension(&self) -> Option<&str> {
105 self.path().extension().and_then(|s| s.to_str())
106 }
107
108 pub fn meta(&self) -> Result<FileMeta, Error> {
113 match &self.inner {
114 FileKind::Embed(embed) => Ok(FileMeta {
115 size: embed.inner.size,
116 modified: embed.inner.modified,
117 }),
118 FileKind::Dynamic(dyn_file) => {
119 let metadata = std::fs::metadata(dyn_file.absolute_path())?;
120 let len = metadata.len();
121 let size = usize::try_from(len).map_err(|_| {
122 std::io::Error::new(std::io::ErrorKind::InvalidData, "file size overflows usize")
123 })?;
124
125 let mtime = metadata.modified()?;
126 let dur = mtime.duration_since(std::time::UNIX_EPOCH).map_err(|_| {
127 std::io::Error::new(
128 std::io::ErrorKind::InvalidData,
129 "file modified time is before UNIX epoch",
130 )
131 })?;
132
133 Ok(FileMeta {
134 size,
135 modified: dur.as_secs(),
136 })
137 }
138 }
139 }
140}
141
142impl PartialEq for File {
144 fn eq(&self, other: &Self) -> bool {
145 self.path() == other.path()
146 }
147}
148
149impl Hash for File {
151 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
152 self.path().hash(state);
153 }
154}
155
156impl Eq for File {}
157
158
159
160#[derive(Debug, Clone)]
162struct EmbedSilo {
163 entries: &'static [(&'static str, EmbedEntry)],
165 root: &'static str,
166}
167
168impl EmbedSilo {
169 pub const fn new(entries: &'static [(&'static str, EmbedEntry)], root: &'static str) -> Self {
171 Self { entries, root }
172 }
173
174 pub fn get_file(&self, path: &str) -> Option<EmbedFile> {
177 self.entries
178 .binary_search_by_key(&path, |(k, _)| k)
179 .ok()
180 .map(|idx| EmbedFile { inner: &self.entries[idx].1 })
181 }
182
183 pub fn iter(&self) -> impl Iterator<Item = File> + '_ {
185 self.entries.iter().map(|(_, entry)| File {
186 inner: FileKind::Embed(EmbedFile { inner: entry }),
187 })
188 }
189}
190
191#[derive(Debug, Clone)]
193struct DynFile {
194 rel_path: Arc<str>,
195 full_path: Arc<str>,
196}
197
198fn normalize_rel_path(path: &str) -> Arc<str> {
199 Arc::from(path.replace('\\', "/"))
201}
202
203impl DynFile {
204 pub fn new<S: AsRef<str>>(full_path: S, rel_path: S) -> Self {
208 Self {
209 rel_path: Arc::from(rel_path.as_ref()),
210 full_path: Arc::from(full_path.as_ref()),
211 }
212 }
213
214 pub fn path(&self) -> &Path {
216 Path::new(&*self.rel_path)
217 }
218
219 pub fn absolute_path(&self) -> &Path {
221 Path::new(&*self.full_path)
222 }
223}
224
225fn get_file_for_root(root: &str, path: &str) -> Option<DynFile> {
227 let root_canon = Path::new(root).canonicalize().ok()?;
230 let normalized_rel = normalize_rel_path(path);
231 let joined = root_canon.join(normalized_rel.as_ref());
232
233 let candidate = joined.canonicalize().ok()?;
234 if !candidate.starts_with(&root_canon) {
235 return None;
236 }
237 if !candidate.is_file() {
238 return None;
239 }
240
241 let rel_path = candidate
242 .strip_prefix(&root_canon)
243 .ok()?
244 .to_str()?
245 .replace('\\', "/");
246 let full_path = candidate.to_str()?;
247 Some(DynFile::new(Arc::from(full_path), Arc::from(rel_path)))
248}
249
250fn iter_root(root: &str) -> impl Iterator<Item = File> {
252 let root_path = PathBuf::from(root);
253 walkdir::WalkDir::new(&root_path)
254 .into_iter()
255 .filter_map(move |entry| {
256 let entry = entry.ok()?;
257 if entry.file_type().is_file() {
258 let relative_path = entry.path().strip_prefix(&root_path).ok()?;
259 Some(File {
260 inner: FileKind::Dynamic(DynFile::new(
261 Arc::from(entry.path().to_str()?),
262 normalize_rel_path(relative_path.to_str()?),
263 )),
264 })
265 } else {
266 None
267 }
268 })
269}
270
271#[derive(Debug, Clone)]
273struct DynamicSilo {
274 root: Arc<str>,
275}
276
277impl DynamicSilo {
278 pub fn new(root: &str) -> Self {
281 Self { root: Arc::from(root) }
282 }
283
284 pub fn get_file(&self, path: &str) -> Option<DynFile> {
287 get_file_for_root(self.root.as_ref(), path)
288 }
289
290 pub fn iter(&self) -> impl Iterator<Item = File> {
293 iter_root(self.root.as_ref())
294 }
295}
296
297#[derive(Debug, Clone)]
300struct StaticSilo {
301 root: &'static str,
302}
303
304impl StaticSilo {
305 pub const fn new(root: &'static str) -> Self {
307 Self { root }
308 }
309
310 pub fn get_file(&self, path: &str) -> Option<DynFile> {
313 get_file_for_root(self.root, path)
314 }
315
316 pub fn iter(&self) -> impl Iterator<Item = File> {
319 iter_root(self.root)
320 }
321}
322
323#[derive(Debug, Clone)]
325enum InnerSilo {
326 Embed(EmbedSilo),
327 Static(StaticSilo),
328 Dynamic(DynamicSilo),
329}
330
331#[derive(Debug, Clone)]
333pub struct Silo {
334 inner: InnerSilo,
335}
336
337impl Silo {
338
339 #[doc(hidden)]
340 pub const fn from_embedded(entries: &'static [(&'static str, EmbedEntry)], root: &'static str) -> Self {
342 Self {
343 inner: InnerSilo::Embed(EmbedSilo::new(entries, root)),
344 }
345 }
346
347 #[doc(hidden)]
348 pub const fn from_static(path: &'static str) -> Self {
350 Self {
351 inner: InnerSilo::Static(StaticSilo::new(path)),
352 }
353 }
354
355 pub fn new(path: &str) -> Self {
357 Self {
358 inner: InnerSilo::Dynamic(DynamicSilo::new(path)),
359 }
360 }
361
362 pub fn into_dynamic(self) -> Self {
365 match self.inner {
366 InnerSilo::Embed(emb_silo) => Self::from_static(emb_silo.root),
367 InnerSilo::Static(_) => self,
368 InnerSilo::Dynamic(_) => self,
369 }
370 }
371
372 pub fn auto_dynamic(self) -> Self {
376 if cfg!(debug_assertions) {
377 self.into_dynamic()
378 } else {
379 self
380 }
381 }
382
383 pub fn is_dynamic(&self) -> bool {
385 matches!(self.inner, InnerSilo::Static(_) | InnerSilo::Dynamic(_))
386 }
387
388 pub fn is_embedded(&self) -> bool {
390 matches!(self.inner, InnerSilo::Embed(_))
391 }
392
393 pub fn get_file(&self, path: &str) -> Option<File> {
396 match &self.inner {
397 InnerSilo::Embed(embed) => embed.get_file(path).map(|f| File {
398 inner: FileKind::Embed(f),
399 }),
400 InnerSilo::Static(dyn_silo) => dyn_silo.get_file(path).map(|f| File {
401 inner: FileKind::Dynamic(f),
402 }),
403 InnerSilo::Dynamic(dyn_silo) => dyn_silo.get_file(path).map(|f| File {
404 inner: FileKind::Dynamic(f),
405 }),
406 }
407 }
408
409 pub fn iter(&self) -> Box<dyn Iterator<Item = File> + '_> {
412 match &self.inner {
413 InnerSilo::Embed(embd) => Box::new(embd.iter()),
414 InnerSilo::Static(dynm) => Box::new(dynm.iter()),
415 InnerSilo::Dynamic(dynm) => Box::new(dynm.iter()),
416 }
417 }
418}
419
420
421
422#[derive(Debug, Clone)]
425pub struct SiloSet {
426 pub silos: Vec<Silo>,
428}
429
430impl SiloSet {
431 pub fn new(dirs: Vec<Silo>) -> Self {
435 Self { silos: dirs }
436 }
437
438
439 pub fn get_file(&self, name: &str) -> Option<File> {
443 for silo in self.silos.iter().rev() {
444 if let Some(file) = silo.get_file(name) {
445 return Some(file);
446 }
447 }
448 None
449 }
450
451 pub fn iter(&self) -> impl Iterator<Item = File> + '_ {
455 self.silos.iter().rev().flat_map(|silo| silo.iter())
456 }
457
458 pub fn iter_override(&self) -> impl Iterator<Item = File> + '_ {
462 let mut history = std::collections::HashSet::new();
463 self.iter().filter(move |file| history.insert(file.clone()))
464 }
465}
466
467
468pub enum FileReader {
470 Embed(std::io::Cursor<&'static [u8]>),
471 Dynamic(std::fs::File),
472}
473
474impl std::io::Read for FileReader {
476 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
477 match self {
478 FileReader::Embed(c) => c.read(buf),
479 FileReader::Dynamic(f) => f.read(buf),
480 }
481 }
482}