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
+68 -25
View File
@@ -3,6 +3,8 @@
//! Directory descriptors retain their absolute virtual path after a directory rename.
//! Relative operations through those descriptors use the old path, not the moved folder.
//! Reopen the moved folder before further relative operations.
//! Object comparisons and metadata hashes use paths, not inode identity (review F12).
//! Hard links remain unsupported, including tempfile persist_noclobber.
use crate::streams::{Reader, Writer};
use crate::*;
use wasmtime::component::{HasData, Linker, Resource, ResourceTable};
@@ -43,8 +45,12 @@ pub fn add_to_linker<T: Send + 'static>(
view: fn(&mut T) -> View<'_>,
) -> wasmtime::Result<()> {
linker.allow_shadowing(true);
types::add_to_linker::<T, Vfs>(linker, view)?;
preopens::add_to_linker::<T, Vfs>(linker, view)?;
let result = (|| {
types::add_to_linker::<T, Vfs>(linker, view)?;
preopens::add_to_linker::<T, Vfs>(linker, view)
})();
linker.allow_shadowing(false);
result?;
Ok(())
}
#[derive(Clone)]
@@ -63,6 +69,8 @@ fn code(e: &io::Error) -> ErrorCode {
ReadOnlyFilesystem => ErrorCode::ReadOnly,
CrossesDevices => ErrorCode::CrossDevice,
Unsupported => ErrorCode::Unsupported,
FileTooLarge => ErrorCode::FileTooLarge,
ResourceBusy => ErrorCode::Busy,
_ => ErrorCode::from(e),
}
}
@@ -78,7 +86,9 @@ fn stat(s: Stat) -> types::DescriptorStat {
})
}
types::DescriptorStat {
type_: if s.directory {
type_: if s.symlink {
types::DescriptorType::SymbolicLink
} else if s.directory {
types::DescriptorType::Directory
} else {
types::DescriptorType::RegularFile
@@ -90,12 +100,19 @@ fn stat(s: Stat) -> types::DescriptorStat {
status_change_timestamp: None,
}
}
fn time(t: types::NewTimestamp) -> Option<SystemTime> {
fn time(t: types::NewTimestamp) -> FsResult<Option<SystemTime>> {
match t {
types::NewTimestamp::NoChange => None,
types::NewTimestamp::Now => Some(SystemTime::now()),
types::NewTimestamp::NoChange => Ok(None),
types::NewTimestamp::Now => Ok(Some(SystemTime::now())),
types::NewTimestamp::Timestamp(t) => {
Some(SystemTime::UNIX_EPOCH + std::time::Duration::new(t.seconds, t.nanoseconds))
let duration = std::time::Duration::from_secs(t.seconds)
.checked_add(std::time::Duration::from_nanos(t.nanoseconds as u64))
.ok_or(ErrorCode::Overflow)?;
Ok(Some(
SystemTime::UNIX_EPOCH
.checked_add(duration)
.ok_or(ErrorCode::Overflow)?,
))
}
}
}
@@ -140,6 +157,7 @@ impl File for Root {
async fn stat(&self) -> Result<Stat> {
Ok(Stat {
directory: true,
symlink: false,
size: 0,
accessed: None,
modified: None,
@@ -211,7 +229,7 @@ impl HostDescriptor for View<'_> {
) -> FsResult<()> {
self.fd(&fd)?
.file
.set_times(time(atim), time(mtim))
.set_times(time(atim)?, time(mtim)?)
.await
.map_err(errno)
}
@@ -257,7 +275,9 @@ impl HostDescriptor for View<'_> {
.into_iter()
.map(|e| DirectoryEntry {
name: e.name,
type_: if e.directory {
type_: if e.symlink {
types::DescriptorType::SymbolicLink
} else if e.directory {
types::DescriptorType::Directory
} else {
types::DescriptorType::RegularFile
@@ -288,9 +308,11 @@ impl HostDescriptor for View<'_> {
path_flags: types::PathFlags,
path: String,
) -> FsResult<types::DescriptorStat> {
let _ = path_flags;
let follow = path_flags.contains(types::PathFlags::SYMLINK_FOLLOW);
let p = self.path(&fd, &path)?;
Ok(stat(self.adapter.mounts.stat(&p).await.map_err(errno)?))
Ok(stat(
self.adapter.mounts.stat(&p, follow).await.map_err(errno)?,
))
}
async fn set_times_at(
&mut self,
@@ -300,20 +322,14 @@ impl HostDescriptor for View<'_> {
atim: types::NewTimestamp,
mtim: types::NewTimestamp,
) -> FsResult<()> {
let _ = path_flags;
let follow = path_flags.contains(types::PathFlags::SYMLINK_FOLLOW);
let p = self.path(&fd, &path)?;
self.adapter
.mounts
.open(
&p,
Open {
write: true,
..Open::default()
},
)
.metadata_file(&p, follow)
.await
.map_err(errno)?
.set_times(time(atim), time(mtim))
.set_times(time(atim)?, time(mtim)?)
.await
.map_err(errno)
}
@@ -336,7 +352,7 @@ impl HostDescriptor for View<'_> {
oflags: OpenFlags,
flags: DescriptorFlags,
) -> FsResult<Resource<types::Descriptor>> {
let _ = path_flags;
let follow = path_flags.contains(types::PathFlags::SYMLINK_FOLLOW);
let path = self.path(&fd, &path)?;
let file = self
.adapter
@@ -344,6 +360,7 @@ impl HostDescriptor for View<'_> {
.open(
&path,
Open {
follow,
write: flags.contains(DescriptorFlags::WRITE),
create: oflags.contains(OpenFlags::CREATE),
exclusive: oflags.contains(OpenFlags::EXCLUSIVE),
@@ -365,8 +382,8 @@ impl HostDescriptor for View<'_> {
fd: Resource<types::Descriptor>,
path: String,
) -> FsResult<String> {
let _ = (fd, path);
Err(ErrorCode::Unsupported.into())
let p = self.path(&fd, &path)?;
self.adapter.mounts.readlink(&p).await.map_err(errno)
}
async fn remove_directory_at(
&mut self,
@@ -449,9 +466,9 @@ impl HostDescriptor for View<'_> {
path_flags: types::PathFlags,
path: String,
) -> FsResult<types::MetadataHashValue> {
let _ = path_flags;
let follow = path_flags.contains(types::PathFlags::SYMLINK_FOLLOW);
let p = self.path(&fd, &path)?;
self.adapter.mounts.stat(&p).await.map_err(errno)?;
self.adapter.mounts.stat(&p, follow).await.map_err(errno)?;
Ok(hash(&p))
}
}
@@ -472,3 +489,29 @@ impl HostDirectoryEntryStream for View<'_> {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
/// Timestamp arithmetic returns overflow instead of panicking on either addition.
#[test]
fn timestamp_overflow() {
for nanoseconds in [0, u32::MAX] {
let error = time(types::NewTimestamp::Timestamp(types::Datetime {
seconds: u64::MAX,
nanoseconds,
}))
.unwrap_err();
assert_eq!(error.downcast().unwrap(), ErrorCode::Overflow);
}
}
/// Portable memory errors preserve their WASI error codes.
#[test]
fn portable_error_codes() {
assert_eq!(
code(&error(io::ErrorKind::FileTooLarge)),
ErrorCode::FileTooLarge
);
assert_eq!(code(&error(io::ErrorKind::ResourceBusy)), ErrorCode::Busy);
}
}