Files
sausage/vfs/src/lib.rs
T
surma 5ce2a2f2bd vfs: host-side WASI preview1 over sausage-vfs (T11)
Preview1 state lives in the host, so Go modules (esbuild) work. esbuild bundles
byte-identical to native; uutils and QuickJS-ng file tests pass. The failed
guest-side adapter attempt (T7) moves to spikes/s7-preview1-adapter/.
2026-11-06 16:34:33 +00:00

107 lines
3.3 KiB
Rust

//! Composable filesystem layers, open files, mount routing, and process IO guards.
pub mod adapter;
pub mod generated;
pub mod host;
pub mod memory;
pub mod mounts;
pub mod observe;
pub mod preview1;
pub mod process;
mod readonly;
mod streams;
pub use async_trait::async_trait;
pub use generated::Generated;
pub use host::Host;
pub use memory::Memory;
pub use mounts::Mounts;
pub use process::Process;
use std::{io, sync::Arc, time::SystemTime};
/// A filesystem result. Error kinds preserve portable filesystem failures.
pub type Result<T> = io::Result<T>;
/// The type and timestamps of a node.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Stat {
pub directory: bool,
pub symlink: bool,
pub size: u64,
pub accessed: Option<SystemTime>,
pub modified: Option<SystemTime>,
}
/// One child of a folder.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Entry {
pub name: String,
pub directory: bool,
pub symlink: bool,
}
/// Options for an open operation. Exclusive applies only with create.
#[derive(Clone, Copy, Debug, Default)]
pub struct Open {
/// Follow the final symbolic link when true.
pub follow: bool,
pub write: bool,
pub create: bool,
pub exclusive: bool,
pub truncate: bool,
pub directory: bool,
}
/// An open file keeps its identity after rename or unlink.
#[async_trait]
pub trait File: Send + Sync {
async fn stat(&self) -> Result<Stat>;
async fn read(&self, offset: u64, length: usize) -> Result<Vec<u8>>;
async fn write(&self, offset: u64, bytes: Vec<u8>) -> Result<usize>;
async fn append(&self, bytes: Vec<u8>) -> Result<usize>;
async fn set_size(&self, size: u64) -> Result<()>;
async fn sync(&self) -> Result<()>;
async fn set_times(
&self,
accessed: Option<SystemTime>,
modified: Option<SystemTime>,
) -> Result<()>;
}
/// One filesystem layer. Paths are normalized, absolute, and relative to its root.
/// Implementations must retain open file identity and return portable IO errors.
#[async_trait]
pub trait Layer: Send + Sync {
async fn stat(&self, path: &str, follow: bool) -> Result<Stat>;
/// Return the target of a symbolic link, or InvalidInput for another node.
async fn readlink(&self, _path: &str) -> Result<String> {
Err(error(io::ErrorKind::InvalidInput))
}
async fn list(&self, path: &str) -> Result<Vec<Entry>>;
async fn open(&self, path: &str, options: Open) -> Result<Arc<dyn File>>;
async fn mkdir(&self, path: &str) -> Result<()>;
async fn remove_file(&self, path: &str) -> Result<()>;
async fn remove_dir(&self, path: &str) -> Result<()>;
async fn rename(&self, from: &str, to: &str) -> Result<()>;
}
pub(crate) fn error(kind: io::ErrorKind) -> io::Error {
kind.into()
}
/// Resolve a path in the virtual namespace, including traversal across mounts.
pub fn resolve(base: &str, path: &str) -> String {
let mut parts = Vec::new();
if !path.starts_with('/') {
parts.extend(base.split('/').filter(|s| !s.is_empty()));
}
for part in path.split('/') {
match part {
"" | "." => {}
".." => {
parts.pop();
}
p => parts.push(p),
}
}
let mut result = format!("/{}", parts.join("/"));
if path.ends_with('/') && result != "/" {
result.push('/');
}
result
}