sausage-vfs: review fixes (fallible growth, symlink flags, stream cancel, sized reads, timestamps, error kinds, linker shadowing)

API: Layer::stat(path, follow), Layer::readlink, Open::follow,
Stat::symlink and Entry::symlink. Process::idle documents the shutdown
order: abort, await the task, await idle, release locks.
This commit is contained in:
2026-10-23 00:49:46 +01:00
parent 7bbcd47837
commit 151550242b
14 changed files with 673 additions and 97 deletions
+75 -15
View File
@@ -1,11 +1,10 @@
//! Capability-sandboxed host files. Every system call runs on the blocking pool.
//! Writable files currently open a second append descriptor by path. A concurrent rename
//! between those opens can make append refer to a different file (review F9).
use crate::*;
use cap_std::fs::{Dir, OpenOptions};
use std::{
io::{Read, Seek, SeekFrom, Write},
path::Path,
sync::Mutex,
};
use cap_fs_ext::{FollowSymlinks, OpenOptionsFollowExt};
use cap_std::fs::{Dir, FileExt, OpenOptions};
use std::{io::Write, path::Path, sync::Mutex};
/// A host directory bound to one process tracker.
#[derive(Clone)]
@@ -36,6 +35,7 @@ fn rel(path: &str) -> String {
fn stat(m: cap_std::fs::Metadata) -> Stat {
Stat {
directory: m.is_dir(),
symlink: m.file_type().is_symlink(),
size: m.len(),
accessed: m.accessed().ok().map(|t| t.into_std()),
modified: m.modified().ok().map(|t| t.into_std()),
@@ -48,9 +48,24 @@ struct Handle {
}
#[async_trait]
impl Layer for Host {
async fn stat(&self, path: &str) -> Result<Stat> {
async fn stat(&self, path: &str, follow: bool) -> Result<Stat> {
let (d, p) = (self.dir.clone(), rel(path));
self.process.blocking(move || d.metadata(p).map(stat)).await
self.process
.blocking(move || {
if follow {
d.metadata(p)
} else {
d.symlink_metadata(p)
}
.map(stat)
})
.await
}
async fn readlink(&self, path: &str) -> Result<String> {
let (d, p) = (self.dir.clone(), rel(path));
self.process
.blocking(move || d.read_link(p).map(|p| p.to_string_lossy().into_owned()))
.await
}
async fn list(&self, path: &str) -> Result<Vec<Entry>> {
let (d, p) = (self.dir.clone(), rel(path));
@@ -63,6 +78,7 @@ impl Layer for Host {
Ok(Entry {
name: e.file_name().to_string_lossy().into_owned(),
directory: e.file_type()?.is_dir(),
symlink: e.file_type()?.is_symlink(),
})
})
.collect::<Result<Vec<_>>>()?;
@@ -83,13 +99,26 @@ impl Layer for Host {
.create(o.create)
.create_new(o.create && o.exclusive)
.truncate(o.truncate);
options.follow(if o.follow {
FollowSymlinks::Yes
} else {
FollowSymlinks::No
});
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))?
{
let mut append = OpenOptions::new();
append.append(true).follow(if o.follow {
FollowSymlinks::Yes
} else {
FollowSymlinks::No
});
d.open_with(&p, &append)?
}
} else {
f.try_clone()?
};
@@ -131,10 +160,14 @@ impl File for Handle {
let f = self.file.clone();
self.process
.blocking(move || {
let mut f = f.lock().unwrap();
f.seek(SeekFrom::Start(offset))?;
let f = f.lock().unwrap();
let mut bytes = Vec::new();
(&mut *f).take(length as u64).read_to_end(&mut bytes)?;
bytes
.try_reserve(length)
.map_err(|_| error(io::ErrorKind::FileTooLarge))?;
bytes.resize(length, 0);
let n = f.read_at(&mut bytes, offset)?;
bytes.truncate(n);
Ok(bytes)
})
.await
@@ -143,9 +176,8 @@ impl File for Handle {
let f = self.file.clone();
self.process
.blocking(move || {
let mut f = f.lock().unwrap();
f.seek(SeekFrom::Start(offset))?;
f.write(&bytes)
let f = f.lock().unwrap();
f.write_at(&bytes, offset)
})
.await
}
@@ -188,3 +220,31 @@ impl File for Handle {
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::{Seek, SeekFrom};
/// Positional reads and writes do not change the descriptor's cursor.
#[tokio::test]
async fn positional_io_keeps_cursor() {
let temp = tempfile::tempdir().unwrap();
std::fs::write(temp.path().join("file"), b"abcd").unwrap();
let host = Host::new(temp.path(), Process::default()).unwrap();
let file = host
.dir
.open_with("file", OpenOptions::new().read(true).write(true))
.unwrap();
let append = file.try_clone().unwrap();
let file = Arc::new(Mutex::new(file));
file.lock().unwrap().seek(SeekFrom::Start(3)).unwrap();
let handle = Handle {
file: file.clone(),
append: Arc::new(Mutex::new(append)),
process: Process::default(),
};
assert_eq!(handle.read(0, 1).await.unwrap(), b"a");
handle.write(1, b"Z".to_vec()).await.unwrap();
assert_eq!(file.lock().unwrap().stream_position().unwrap(), 3);
}
}