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
+190
View File
@@ -0,0 +1,190 @@
//! Capability-sandboxed host files. Every system call runs on the blocking pool.
use crate::*;
use cap_std::fs::{Dir, OpenOptions};
use std::{
io::{Read, Seek, SeekFrom, Write},
path::Path,
sync::Mutex,
};
/// A host directory bound to one process tracker.
#[derive(Clone)]
pub struct Host {
dir: Arc<Dir>,
process: Process,
}
impl Host {
/// Open the sandbox root. Call this outside an asynchronous worker.
pub fn new(path: impl AsRef<Path>, process: Process) -> Result<Self> {
Ok(Self {
dir: Arc::new(Dir::open_ambient_dir(path, cap_std::ambient_authority())?),
process,
})
}
/// Share a sandbox with a different process tracker.
pub fn for_process(&self, process: Process) -> Self {
Self {
dir: self.dir.clone(),
process,
}
}
}
fn rel(path: &str) -> String {
let p = path.trim_start_matches('/');
if p.is_empty() { ".".into() } else { p.into() }
}
fn stat(m: cap_std::fs::Metadata) -> Stat {
Stat {
directory: m.is_dir(),
size: m.len(),
accessed: m.accessed().ok().map(|t| t.into_std()),
modified: m.modified().ok().map(|t| t.into_std()),
}
}
struct Handle {
file: Arc<Mutex<cap_std::fs::File>>,
append: Arc<Mutex<cap_std::fs::File>>,
process: Process,
}
#[async_trait]
impl Layer for Host {
async fn stat(&self, path: &str) -> Result<Stat> {
let (d, p) = (self.dir.clone(), rel(path));
self.process.blocking(move || d.metadata(p).map(stat)).await
}
async fn list(&self, path: &str) -> Result<Vec<Entry>> {
let (d, p) = (self.dir.clone(), rel(path));
self.process
.blocking(move || {
let mut entries = d
.read_dir(p)?
.map(|e| {
let e = e?;
Ok(Entry {
name: e.file_name().to_string_lossy().into_owned(),
directory: e.file_type()?.is_dir(),
})
})
.collect::<Result<Vec<_>>>()?;
entries.sort_by(|a, b| a.name.cmp(&b.name));
Ok(entries)
})
.await
}
async fn open(&self, path: &str, o: Open) -> Result<Arc<dyn File>> {
let (d, p) = (self.dir.clone(), rel(path));
let (file, append) = self
.process
.blocking(move || {
let mut options = OpenOptions::new();
options
.read(true)
.write(o.write)
.create(o.create)
.create_new(o.create && o.exclusive)
.truncate(o.truncate);
let f = d.open_with(&p, &options)?;
let directory = f.metadata()?.is_dir();
if o.directory && !directory {
return Err(error(io::ErrorKind::NotADirectory));
}
let append = if o.write && !directory {
d.open_with(&p, OpenOptions::new().append(true))?
} else {
f.try_clone()?
};
Ok((f, append))
})
.await?;
Ok(Arc::new(Handle {
file: Arc::new(Mutex::new(file)),
append: Arc::new(Mutex::new(append)),
process: self.process.clone(),
}))
}
async fn mkdir(&self, path: &str) -> Result<()> {
let (d, p) = (self.dir.clone(), rel(path));
self.process.blocking(move || d.create_dir(p)).await
}
async fn remove_file(&self, path: &str) -> Result<()> {
let (d, p) = (self.dir.clone(), rel(path));
self.process.blocking(move || d.remove_file(p)).await
}
async fn remove_dir(&self, path: &str) -> Result<()> {
let (d, p) = (self.dir.clone(), rel(path));
self.process.blocking(move || d.remove_dir(p)).await
}
async fn rename(&self, from: &str, to: &str) -> Result<()> {
let (d, a, b) = (self.dir.clone(), rel(from), rel(to));
self.process.blocking(move || d.rename(a, &d, b)).await
}
}
#[async_trait]
impl File for Handle {
async fn stat(&self) -> Result<Stat> {
let f = self.file.clone();
self.process
.blocking(move || f.lock().unwrap().metadata().map(stat))
.await
}
async fn read(&self, offset: u64, length: usize) -> Result<Vec<u8>> {
let f = self.file.clone();
self.process
.blocking(move || {
let mut f = f.lock().unwrap();
f.seek(SeekFrom::Start(offset))?;
let mut bytes = Vec::new();
(&mut *f).take(length as u64).read_to_end(&mut bytes)?;
Ok(bytes)
})
.await
}
async fn write(&self, offset: u64, bytes: Vec<u8>) -> Result<usize> {
let f = self.file.clone();
self.process
.blocking(move || {
let mut f = f.lock().unwrap();
f.seek(SeekFrom::Start(offset))?;
f.write(&bytes)
})
.await
}
async fn append(&self, bytes: Vec<u8>) -> Result<usize> {
let f = self.append.clone();
self.process
.blocking(move || f.lock().unwrap().write(&bytes))
.await
}
async fn set_size(&self, size: u64) -> Result<()> {
let f = self.file.clone();
self.process
.blocking(move || f.lock().unwrap().set_len(size))
.await
}
async fn sync(&self) -> Result<()> {
let f = self.file.clone();
self.process
.blocking(move || f.lock().unwrap().sync_all())
.await
}
async fn set_times(
&self,
accessed: Option<SystemTime>,
modified: Option<SystemTime>,
) -> Result<()> {
let f = self.file.clone();
self.process
.blocking(move || {
let f = f.lock().unwrap().try_clone()?.into_std();
let mut times = std::fs::FileTimes::new();
if let Some(t) = accessed {
times = times.set_accessed(t);
}
if let Some(t) = modified {
times = times.set_modified(t);
}
f.set_times(times)
})
.await
}
}