//! The treehouse virtual file system. //! //! Unlike traditional file systems, there is no separation between directories and files. //! Instead, our file system is based on _entries_, which may have specific, optional, well-typed //! metadata attached to them. //! A directory is formed by returning a list of paths from [`dir`][Dir::dir], and a file is //! formed by returning `Some` from [`content`][Dir::content]. //! //! This makes using the file system simpler, as you do not have to differentiate between different //! entry kinds. All paths act as if they _could_ return byte content, and all paths act as if they //! _could_ have children. //! //! # Composability //! //! [`Dir`]s are composable. The [`Dir`] itself starts off with the root path ([`VPath::ROOT`]), //! which may contain further [`dir`][Dir::dir] entries, or content by itself. //! This makes it possible to nest a [`Dir`] under another [`Dir`]. //! //! Additionally, there's also the inverse operation, [`Cd`] (named after the `cd` //! _change directory_ shell command), which returns a [`Dir`] viewing a subpath within another //! [`Dir`]. //! //! # Building directories //! //! In-memory directories can be composed using the following primitives: //! //! - [`BufferedFile`] - root path content is the provided byte vector. //! - [`MemDir`] - a [`Dir`] containing a single level of other [`Dir`]s inside. //! //! Additionally, for interfacing with the OS file system, [`PhysicalDir`] is available, //! representing a directory stored on the disk. //! //! # Virtual paths //! //! Entries within directories are referenced using [`VPath`]s (**v**irtual **path**s). //! A virtual path is composed out of any amount of `/`-separated components. //! //! There are no special directories like `.` and `..` (those are just normal entries, though using //! them is discouraged). [`VPath`]s are always relative to the root of the [`Dir`] you're querying. //! //! A leading or trailing slash is not allowed, because they would have no meaning. //! //! [`VPath`] also has an owned version, [`VPathBuf`]. use std::{ any::TypeId, fmt::{self, Debug}, ops::{ControlFlow, Deref}, string::FromUtf8Error, sync::Arc, }; mod anchored; pub mod asynch; mod cd; mod content_cache; mod content_version_cache; mod edit; mod file; mod html_canonicalize; mod image_size_cache; mod mem_dir; mod overlay; mod path; mod physical; pub use anchored::*; pub use cd::*; pub use content_cache::*; pub use content_version_cache::*; pub use edit::*; pub use file::*; pub use html_canonicalize::*; pub use image_size_cache::*; pub use mem_dir::*; pub use overlay::*; pub use path::*; pub use physical::*; pub trait Dir: Debug { fn query(&self, path: &VPath, query: &mut Query); } pub trait Fork {} pub fn query<'a, T>(dir: &'a (impl Dir + ?Sized), path: &VPath) -> Option where T: 'static + Fork, { let mut slot = TaggedOption::<'a, tags::Value>(None); dir.query(path, Query::new(&mut slot)); slot.0 } #[repr(transparent)] pub struct Query<'a> { erased: dyn Erased<'a> + 'a, } impl<'a> Query<'a> { fn new<'b>(erased: &'b mut (dyn Erased<'a> + 'a)) -> &'b mut Query<'a> { unsafe { &mut *(erased as *mut dyn Erased<'a> as *mut Query<'a>) } } pub fn provide(&mut self, f: impl FnOnce() -> T) where T: 'static + Fork, { if let Some(result @ TaggedOption(None)) = self.erased.downcast_mut::>() { result.0 = Some(f()); } } pub fn try_provide(&mut self, f: impl FnOnce() -> Option) where T: 'static + Fork, { if let Some(result @ TaggedOption(None)) = self.erased.downcast_mut::>() { result.0 = f(); } } } mod tags { use std::marker::PhantomData; pub trait Type<'a>: Sized + 'static { type Reified: 'a; } pub struct Value(PhantomData) where T: 'static; impl Type<'_> for Value where T: 'static, { type Reified = T; } } #[repr(transparent)] struct TaggedOption<'a, I: tags::Type<'a>>(Option); #[expect(clippy::missing_safety_doc)] unsafe trait Erased<'a>: 'a { fn tag_id(&self) -> TypeId; } unsafe impl<'a, I: tags::Type<'a>> Erased<'a> for TaggedOption<'a, I> { fn tag_id(&self) -> TypeId { TypeId::of::() } } impl<'a> dyn Erased<'a> + 'a { fn downcast_mut(&mut self) -> Option<&mut TaggedOption<'a, I>> where I: tags::Type<'a>, { if self.tag_id() == TypeId::of::() { // SAFETY: Just checked whether we're pointing to an I. Some(unsafe { &mut *(self as *mut Self).cast::>() }) } else { None } } } impl Dir for () { fn query(&self, _path: &VPath, _query: &mut Query) { // Noop implementation. } } impl Dir for &T where T: Dir, { fn query(&self, path: &VPath, query: &mut Query) { (**self).query(path, query) } } #[derive(Clone)] pub struct DynDir { arc: Arc, } impl Dir for DynDir { fn query(&self, path: &VPath, query: &mut Query) { self.arc.query(path, query); } } impl fmt::Debug for DynDir { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Debug::fmt(&*self.arc, f) } } impl Deref for DynDir { type Target = dyn Dir + Send + Sync; fn deref(&self) -> &Self::Target { &*self.arc } } pub trait ToDynDir { fn to_dyn(self) -> DynDir; } impl ToDynDir for T where T: Dir + Send + Sync + 'static, { fn to_dyn(self) -> DynDir { DynDir { arc: Arc::new(self), } } } pub trait AnchoredAtExt { fn anchored_at(self, at: VPathBuf) -> Anchored where Self: Sized; } impl AnchoredAtExt for T where T: Dir, { fn anchored_at(self, at: VPathBuf) -> Anchored { Anchored::new(self, at) } } /// List of child entries under a directory. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct Entries(pub Vec); /// Byte content in an entry. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct Content { /// Media type string. kind: String, bytes: Vec, } /// Abstract version of an entry. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct ContentVersion { pub string: String, } /// Path relative to `config.site` indicating where the file will be available once served. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct Anchor { pub path: VPathBuf, } /// Size of image entries. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub struct ImageSize { pub width: u32, pub height: u32, } impl Content { pub fn new(kind: impl Into, bytes: Vec) -> Self { Self { kind: kind.into(), bytes, } } pub fn kind(&self) -> &str { &self.kind } pub fn bytes(self) -> Vec { self.bytes } pub fn string(self) -> Result { String::from_utf8(self.bytes()) } } impl Fork for Entries {} impl Fork for Content {} impl Fork for ContentVersion {} impl Fork for Anchor {} impl Fork for ImageSize {} impl Fork for EditPath {} pub fn entries(dir: &dyn Dir, path: &VPath) -> Vec { query::(dir, path).map(|e| e.0).unwrap_or_default() } pub fn walk_dir_rec(dir: &dyn Dir, path: &VPath, f: &mut dyn FnMut(&VPath) -> ControlFlow<(), ()>) { for entry in entries(dir, path) { match f(&entry) { ControlFlow::Continue(_) => (), ControlFlow::Break(_) => return, } walk_dir_rec(dir, &entry, f); } } pub fn url(site: &str, dir: &dyn Dir, path: &VPath) -> Option { let anchor = query::(dir, path)?; if let Some(version) = query::(dir, path) { Some(format!("{}/{}?v={}", site, anchor.path, version.string)) } else { Some(format!("{}/{}", site, anchor.path)) } }