1pub 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
10pub use rust_silos_macros::embed_silo;
11
12
13#[derive(Debug, Error)]
15pub enum Error {
16 #[error("Failed to decode file contents: {source}")]
17 DecodeError {
18 #[from]
19 source: std::string::FromUtf8Error,
20 },
21 #[error("File not found")]
22 NotFound,
23 #[error("I/O error: {source}")]
24 IoError {
25 #[from]
26 source: std::io::Error,
27 },
28}
29
30
31#[derive(Debug)]
33pub struct EmbedEntry {
34 pub path: &'static str,
35 pub contents: &'static [u8],
36 pub size: usize,
37 pub modified: u64,
38}
39
40#[derive(Debug, Copy, Clone, Eq, PartialEq)]
42pub struct FileMeta {
43 pub size: usize,
44 pub modified: u64,
46}
47
48#[derive(Copy, Clone, Debug)]
50struct EmbedFile {
51 inner: &'static EmbedEntry,
52}
53
54impl EmbedFile {
55 pub fn path(&self) -> &Path {
57 Path::new(self.inner.path)
58 }
59}
60
61#[derive(Debug, Clone)]
63enum FileKind {
64 Embed(EmbedFile),
65 Dynamic(DynFile),
66}
67
68#[derive(Debug, Clone)]
70pub struct File {
71 inner: FileKind,
72}
73
74impl File {
75 pub fn reader(&self) -> Result<FileReader, Error> {
77 match &self.inner {
78 FileKind::Embed(embed) => Ok(FileReader::Embed(Cursor::new(embed.inner.contents))),
79 FileKind::Dynamic(dyn_file) => Ok(FileReader::Dynamic(std::fs::File::open(
80 dyn_file.absolute_path(),
81 )?)),
82 }
83 }
84
85 pub fn path(&self) -> &Path {
87 match &self.inner {
88 FileKind::Embed(embed) => embed.path(),
89 FileKind::Dynamic(dyn_file) => dyn_file.path(),
90 }
91 }
92
93 pub fn is_embedded(&self) -> bool {
95 matches!(self.inner, FileKind::Embed(_))
96 }
97
98 pub fn absolute_path(&self) -> Option<&Path> {
100 match &self.inner {
101 FileKind::Embed(_) => None,
102 FileKind::Dynamic(dyn_file) => Some(dyn_file.absolute_path()),
103 }
104 }
105
106 pub fn extension(&self) -> Option<&str> {
108 self.path().extension().and_then(|s| s.to_str())
109 }
110
111 pub fn meta(&self) -> Result<FileMeta, Error> {
116 match &self.inner {
117 FileKind::Embed(embed) => Ok(FileMeta {
118 size: embed.inner.size,
119 modified: embed.inner.modified,
120 }),
121 FileKind::Dynamic(dyn_file) => {
122 let metadata = std::fs::metadata(dyn_file.absolute_path())?;
123 let len = metadata.len();
124 let size = usize::try_from(len).map_err(|_| {
125 std::io::Error::new(std::io::ErrorKind::InvalidData, "file size overflows usize")
126 })?;
127
128 let mtime = metadata.modified()?;
129 let dur = mtime.duration_since(std::time::UNIX_EPOCH).map_err(|_| {
130 std::io::Error::new(
131 std::io::ErrorKind::InvalidData,
132 "file modified time is before UNIX epoch",
133 )
134 })?;
135
136 Ok(FileMeta {
137 size,
138 modified: dur.as_secs(),
139 })
140 }
141 }
142 }
143}
144
145impl PartialEq for File {
147 fn eq(&self, other: &Self) -> bool {
148 self.path() == other.path()
149 }
150}
151
152impl Hash for File {
154 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
155 self.path().hash(state);
156 }
157}
158
159impl Eq for File {}
160
161
162
163#[derive(Debug, Clone)]
165struct EmbedSilo {
166 map: &'static phf::Map<&'static str, EmbedEntry>,
167 root: &'static str,
168}
169
170impl EmbedSilo {
171 pub const fn new(map: &'static phf::Map<&'static str, EmbedEntry>, root: &'static str) -> Self {
173 Self { map, root }
174 }
175
176 pub fn get_file(&self, path: &str) -> Option<EmbedFile> {
179 self.map.get(path).map(|entry| EmbedFile { inner: entry })
180 }
181
182 pub fn iter(&self) -> impl Iterator<Item = File> + '_ {
184 self.map.values().map(|entry| File {
185 inner: FileKind::Embed(EmbedFile { inner: entry }),
186 })
187 }
188}
189
190#[derive(Debug, Clone)]
192struct DynFile {
193 rel_path: Arc<str>,
194 full_path: Arc<str>,
195}
196
197fn normalize_rel_path(path: &str) -> Arc<str> {
198 Arc::from(path.replace('\\', "/"))
200}
201
202impl DynFile {
203 pub fn new<S: AsRef<str>>(full_path: S, rel_path: S) -> Self {
207 Self {
208 rel_path: Arc::from(rel_path.as_ref()),
209 full_path: Arc::from(full_path.as_ref()),
210 }
211 }
212
213 pub fn path(&self) -> &Path {
215 Path::new(&*self.rel_path)
216 }
217
218 pub fn absolute_path(&self) -> &Path {
220 Path::new(&*self.full_path)
221 }
222}
223
224fn get_file_for_root(root: &str, path: &str) -> Option<DynFile> {
226 let root_canon = Path::new(root).canonicalize().ok()?;
229 let normalized_rel = normalize_rel_path(path);
230 let joined = root_canon.join(normalized_rel.as_ref());
231
232 let candidate = joined.canonicalize().ok()?;
233 if !candidate.starts_with(&root_canon) {
234 return None;
235 }
236 if !candidate.is_file() {
237 return None;
238 }
239
240 let rel_path = candidate
241 .strip_prefix(&root_canon)
242 .ok()?
243 .to_str()?
244 .replace('\\', "/");
245 let full_path = candidate.to_str()?;
246 Some(DynFile::new(Arc::from(full_path), Arc::from(rel_path)))
247}
248
249fn iter_root(root: &str) -> impl Iterator<Item = File> {
251 let root_path = PathBuf::from(root);
252 walkdir::WalkDir::new(&root_path)
253 .into_iter()
254 .filter_map(move |entry| {
255 let entry = entry.ok()?;
256 if entry.file_type().is_file() {
257 let relative_path = entry.path().strip_prefix(&root_path).ok()?;
258 Some(File {
259 inner: FileKind::Dynamic(DynFile::new(
260 Arc::from(entry.path().to_str()?),
261 normalize_rel_path(relative_path.to_str()?),
262 )),
263 })
264 } else {
265 None
266 }
267 })
268}
269
270#[derive(Debug, Clone)]
272struct DynamicSilo {
273 root: Arc<str>,
274}
275
276impl DynamicSilo {
277 pub fn new(root: &str) -> Self {
280 Self { root: Arc::from(root) }
281 }
282
283 pub fn get_file(&self, path: &str) -> Option<DynFile> {
286 get_file_for_root(self.root.as_ref(), path)
287 }
288
289 pub fn iter(&self) -> impl Iterator<Item = File> {
292 iter_root(self.root.as_ref())
293 }
294}
295
296#[derive(Debug, Clone)]
299struct StaticSilo {
300 root: &'static str,
301}
302
303impl StaticSilo {
304 pub const fn new(root: &'static str) -> Self {
306 Self { root }
307 }
308
309 pub fn get_file(&self, path: &str) -> Option<DynFile> {
312 get_file_for_root(self.root, path)
313 }
314
315 pub fn iter(&self) -> impl Iterator<Item = File> {
318 iter_root(self.root)
319 }
320}
321
322#[derive(Debug, Clone)]
324enum InnerSilo {
325 Embed(EmbedSilo),
326 Static(StaticSilo),
327 Dynamic(DynamicSilo),
328}
329
330#[derive(Debug, Clone)]
332pub struct Silo {
333 inner: InnerSilo,
334}
335
336impl Silo {
337
338 #[doc(hidden)]
339 pub const fn from_embedded(phf_map: &'static phf::Map<&'static str, EmbedEntry>, root: &'static str) -> Self {
341 Self {
342 inner: InnerSilo::Embed(EmbedSilo::new(phf_map, root)),
343 }
344 }
345
346 #[doc(hidden)]
347 pub const fn from_static(path: &'static str) -> Self {
349 Self {
350 inner: InnerSilo::Static(StaticSilo::new(path)),
351 }
352 }
353
354 pub fn new(path: &str) -> Self {
356 Self {
357 inner: InnerSilo::Dynamic(DynamicSilo::new(path)),
358 }
359 }
360
361 pub fn into_dynamic(self) -> Self {
364 match self.inner {
365 InnerSilo::Embed(emb_silo) => Self::from_static(emb_silo.root),
366 InnerSilo::Static(_) => self,
367 InnerSilo::Dynamic(_) => self,
368 }
369 }
370
371 pub fn auto_dynamic(self) -> Self {
375 if cfg!(debug_assertions) {
376 self.into_dynamic()
377 } else {
378 self
379 }
380 }
381
382 pub fn is_dynamic(&self) -> bool {
384 matches!(self.inner, InnerSilo::Static(_) | InnerSilo::Dynamic(_))
385 }
386
387 pub fn is_embedded(&self) -> bool {
389 matches!(self.inner, InnerSilo::Embed(_))
390 }
391
392 pub fn get_file(&self, path: &str) -> Option<File> {
395 match &self.inner {
396 InnerSilo::Embed(embed) => embed.get_file(path).map(|f| File {
397 inner: FileKind::Embed(f),
398 }),
399 InnerSilo::Static(dyn_silo) => dyn_silo.get_file(path).map(|f| File {
400 inner: FileKind::Dynamic(f),
401 }),
402 InnerSilo::Dynamic(dyn_silo) => dyn_silo.get_file(path).map(|f| File {
403 inner: FileKind::Dynamic(f),
404 }),
405 }
406 }
407
408 pub fn iter(&self) -> Box<dyn Iterator<Item = File> + '_> {
411 match &self.inner {
412 InnerSilo::Embed(embd) => Box::new(embd.iter()),
413 InnerSilo::Static(dynm) => Box::new(dynm.iter()),
414 InnerSilo::Dynamic(dynm) => Box::new(dynm.iter()),
415 }
416 }
417}
418
419
420
421#[derive(Debug, Clone)]
424pub struct SiloSet {
425 pub silos: Vec<Silo>,
427}
428
429impl SiloSet {
430 pub fn new(dirs: Vec<Silo>) -> Self {
434 Self { silos: dirs }
435 }
436
437
438 pub fn get_file(&self, name: &str) -> Option<File> {
442 for silo in self.silos.iter().rev() {
443 if let Some(file) = silo.get_file(name) {
444 return Some(file);
445 }
446 }
447 None
448 }
449
450 pub fn iter(&self) -> impl Iterator<Item = File> + '_ {
454 self.silos.iter().rev().flat_map(|silo| silo.iter())
455 }
456
457 pub fn iter_override(&self) -> impl Iterator<Item = File> + '_ {
461 let mut history = std::collections::HashSet::new();
462 self.iter().filter(move |file| history.insert(file.clone()))
463 }
464}
465
466
467pub enum FileReader {
469 Embed(std::io::Cursor<&'static [u8]>),
470 Dynamic(std::fs::File),
471}
472
473impl std::io::Read for FileReader {
475 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
476 match self {
477 FileReader::Embed(c) => c.read(buf),
478 FileReader::Dynamic(f) => f.read(buf),
479 }
480 }
481}