treehouse/src/vfs.rs

322 lines
8 KiB
Rust
Raw Normal View History

//! 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<T>
where
T: 'static + Fork,
{
let mut slot = TaggedOption::<'a, tags::Value<T>>(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<T>(&mut self, f: impl FnOnce() -> T)
where
T: 'static + Fork,
{
if let Some(result @ TaggedOption(None)) = self.erased.downcast_mut::<tags::Value<T>>() {
result.0 = Some(f());
}
}
pub fn try_provide<T>(&mut self, f: impl FnOnce() -> Option<T>)
where
T: 'static + Fork,
{
if let Some(result @ TaggedOption(None)) = self.erased.downcast_mut::<tags::Value<T>>() {
result.0 = f();
}
}
}
mod tags {
use std::marker::PhantomData;
pub trait Type<'a>: Sized + 'static {
type Reified: 'a;
}
pub struct Value<T>(PhantomData<T>)
where
T: 'static;
impl<T> Type<'_> for Value<T>
where
T: 'static,
{
type Reified = T;
}
}
#[repr(transparent)]
struct TaggedOption<'a, I: tags::Type<'a>>(Option<I::Reified>);
#[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::<I>()
}
}
impl<'a> dyn Erased<'a> + 'a {
fn downcast_mut<I>(&mut self) -> Option<&mut TaggedOption<'a, I>>
where
I: tags::Type<'a>,
{
if self.tag_id() == TypeId::of::<I>() {
// SAFETY: Just checked whether we're pointing to an I.
Some(unsafe { &mut *(self as *mut Self).cast::<TaggedOption<'a, I>>() })
} else {
None
}
}
}
impl Dir for () {
fn query(&self, _path: &VPath, _query: &mut Query) {
// Noop implementation.
}
}
impl<T> 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<dyn Dir + Send + Sync>,
}
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<T> 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<Self>
where
Self: Sized;
}
impl<T> AnchoredAtExt for T
where
T: Dir,
{
fn anchored_at(self, at: VPathBuf) -> Anchored<Self> {
Anchored::new(self, at)
}
}
/// List of child entries under a directory.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Entries(pub Vec<VPathBuf>);
/// Byte content in an entry.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Content {
/// Media type string. <https://en.wikipedia.org/wiki/Media_type>
kind: String,
bytes: Vec<u8>,
}
/// 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<String>, bytes: Vec<u8>) -> Self {
Self {
kind: kind.into(),
bytes,
}
}
pub fn kind(&self) -> &str {
&self.kind
}
pub fn bytes(self) -> Vec<u8> {
self.bytes
}
pub fn string(self) -> Result<String, FromUtf8Error> {
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<VPathBuf> {
query::<Entries>(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<String> {
let anchor = query::<Anchor>(dir, path)?;
if let Some(version) = query::<ContentVersion>(dir, path) {
Some(format!("{}/{}?v={}", site, anchor.path, version.string))
} else {
Some(format!("{}/{}", site, anchor.path))
}
}