sausage-vfs: layers (host, memory, generated, mounts), process guards, wasi:filesystem adapter

Checkpoint. More tests and the review findings follow.
This commit is contained in:
2026-10-23 00:08:02 +01:00
parent 01712e745a
commit 0a479b5800
15 changed files with 1932 additions and 1 deletions
+257
View File
@@ -0,0 +1,257 @@
//! Shared in-memory directories and identity-preserving open files.
use crate::*;
use std::{collections::BTreeMap, io::ErrorKind as E, sync::Mutex};
#[derive(Clone)]
enum Node {
Dir,
File(Arc<Mutex<Data>>),
}
struct Data {
bytes: Vec<u8>,
accessed: Option<SystemTime>,
modified: Option<SystemTime>,
}
/// A shareable memory filesystem. Clones refer to the same tree.
#[derive(Clone)]
pub struct Memory {
nodes: Arc<Mutex<BTreeMap<String, Node>>>,
}
impl Default for Memory {
fn default() -> Self {
Self {
nodes: Arc::new(Mutex::new(BTreeMap::from([("/".into(), Node::Dir)]))),
}
}
}
fn parent(path: &str) -> &str {
path.rsplit_once('/')
.map(|(p, _)| if p.is_empty() { "/" } else { p })
.unwrap_or("/")
}
fn directory(nodes: &BTreeMap<String, Node>, path: &str) -> Result<()> {
match nodes.get(path) {
Some(Node::Dir) => Ok(()),
Some(_) => Err(error(E::NotADirectory)),
None => Err(error(E::NotFound)),
}
}
fn metadata(node: &Node) -> Stat {
match node {
Node::Dir => Stat {
directory: true,
size: 0,
accessed: None,
modified: None,
},
Node::File(data) => {
let d = data.lock().unwrap();
Stat {
directory: false,
size: d.bytes.len() as u64,
accessed: d.accessed,
modified: d.modified,
}
}
}
}
struct Handle {
node: Node,
write: bool,
}
#[async_trait]
impl Layer for Memory {
async fn stat(&self, path: &str) -> Result<Stat> {
self.nodes
.lock()
.unwrap()
.get(path)
.map(metadata)
.ok_or_else(|| error(E::NotFound))
}
async fn list(&self, path: &str) -> Result<Vec<Entry>> {
let nodes = self.nodes.lock().unwrap();
directory(&nodes, path)?;
Ok(nodes
.iter()
.filter(|(p, _)| p.as_str() != path && parent(p) == path)
.map(|(p, n)| Entry {
name: p.rsplit('/').next().unwrap().into(),
directory: matches!(n, Node::Dir),
})
.collect())
}
async fn open(&self, path: &str, o: Open) -> Result<Arc<dyn File>> {
let mut nodes = self.nodes.lock().unwrap();
if o.truncate && !o.write {
return Err(error(E::InvalidInput));
}
if nodes.contains_key(path) && o.create && o.exclusive {
return Err(error(E::AlreadyExists));
}
if !nodes.contains_key(path) {
if !o.create || o.directory {
return Err(error(E::NotFound));
}
directory(&nodes, parent(path))?;
nodes.insert(
path.into(),
Node::File(Arc::new(Mutex::new(Data {
bytes: vec![],
accessed: None,
modified: None,
}))),
);
}
let node = nodes[path].clone();
match &node {
Node::Dir if o.write || o.truncate => return Err(error(E::IsADirectory)),
Node::File(_) if o.directory => return Err(error(E::NotADirectory)),
Node::File(d) if o.truncate => d.lock().unwrap().bytes.clear(),
_ => {}
}
Ok(Arc::new(Handle {
node,
write: o.write,
}))
}
async fn mkdir(&self, path: &str) -> Result<()> {
let mut nodes = self.nodes.lock().unwrap();
if nodes.contains_key(path) {
return Err(error(E::AlreadyExists));
}
directory(&nodes, parent(path))?;
nodes.insert(path.into(), Node::Dir);
Ok(())
}
async fn remove_file(&self, path: &str) -> Result<()> {
let mut nodes = self.nodes.lock().unwrap();
match nodes.get(path) {
None => return Err(error(E::NotFound)),
Some(Node::Dir) => return Err(error(E::IsADirectory)),
_ => {}
}
nodes.remove(path);
Ok(())
}
async fn remove_dir(&self, path: &str) -> Result<()> {
let mut nodes = self.nodes.lock().unwrap();
directory(&nodes, path)?;
if path == "/" {
return Err(error(E::ResourceBusy));
}
if nodes.keys().any(|p| p != path && parent(p) == path) {
return Err(error(E::DirectoryNotEmpty));
}
nodes.remove(path);
Ok(())
}
async fn rename(&self, from: &str, to: &str) -> Result<()> {
let mut nodes = self.nodes.lock().unwrap();
let node = nodes.get(from).cloned().ok_or_else(|| error(E::NotFound))?;
directory(&nodes, parent(to))?;
if from == to {
return Ok(());
}
if from == "/" || to == "/" {
return Err(error(E::ResourceBusy));
}
if to.starts_with(&format!("{from}/")) {
return Err(error(E::InvalidInput));
}
if let Some(target) = nodes.get(to) {
match (&node, target) {
(Node::Dir, Node::File(_)) => return Err(error(E::NotADirectory)),
(Node::File(_), Node::Dir) => return Err(error(E::IsADirectory)),
(Node::Dir, Node::Dir) if nodes.keys().any(|p| p != to && parent(p) == to) => {
return Err(error(E::DirectoryNotEmpty));
}
_ => {}
}
}
let prefix = format!("{from}/");
let moved: Vec<_> = nodes
.keys()
.filter(|p| p.as_str() == from || p.starts_with(&prefix))
.cloned()
.collect();
for p in moved {
let n = nodes.remove(&p).unwrap();
nodes.insert(format!("{to}{}", &p[from.len()..]), n);
}
Ok(())
}
}
impl Handle {
fn data(&self) -> Result<&Mutex<Data>> {
match &self.node {
Node::File(d) => Ok(d),
_ => Err(error(E::IsADirectory)),
}
}
fn writable(&self) -> Result<&Mutex<Data>> {
if !self.write {
return Err(error(E::PermissionDenied));
}
self.data()
}
}
#[async_trait]
impl File for Handle {
async fn stat(&self) -> Result<Stat> {
Ok(metadata(&self.node))
}
async fn read(&self, offset: u64, length: usize) -> Result<Vec<u8>> {
let d = self.data()?.lock().unwrap();
let start = usize::try_from(offset)
.unwrap_or(usize::MAX)
.min(d.bytes.len());
Ok(d.bytes[start..start.saturating_add(length).min(d.bytes.len())].to_vec())
}
async fn write(&self, offset: u64, bytes: Vec<u8>) -> Result<usize> {
let mut d = self.writable()?.lock().unwrap();
let start = usize::try_from(offset).map_err(|_| error(E::InvalidInput))?;
let end = start
.checked_add(bytes.len())
.ok_or_else(|| error(E::InvalidInput))?;
if !bytes.is_empty() {
if end > d.bytes.len() {
d.bytes.resize(end, 0);
}
d.bytes[start..end].copy_from_slice(&bytes);
}
Ok(bytes.len())
}
async fn append(&self, bytes: Vec<u8>) -> Result<usize> {
self.writable()?
.lock()
.unwrap()
.bytes
.extend_from_slice(&bytes);
Ok(bytes.len())
}
async fn set_size(&self, size: u64) -> Result<()> {
self.writable()?.lock().unwrap().bytes.resize(
usize::try_from(size).map_err(|_| error(E::InvalidInput))?,
0,
);
Ok(())
}
async fn sync(&self) -> Result<()> {
Ok(())
}
async fn set_times(
&self,
accessed: Option<SystemTime>,
modified: Option<SystemTime>,
) -> Result<()> {
let mut d = self.writable()?.lock().unwrap();
if accessed.is_some() {
d.accessed = accessed;
}
if modified.is_some() {
d.modified = modified;
}
Ok(())
}
}