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);
}
}
+10 -3
View File
@@ -2,7 +2,8 @@
use crate::*;
use std::{collections::BTreeMap, io::ErrorKind as E};
type Generator = Arc<dyn Fn() -> Result<Vec<u8>> + Send + Sync>;
/// A tree of generated files. Parent folders are implicit.
/// A static tree of generated files. Parent folders are implicit.
/// Generators must be cheap and must not block. Stat can invoke them as well as open.
#[derive(Default)]
pub struct Generated {
files: BTreeMap<String, Generator>,
@@ -29,7 +30,7 @@ fn readonly<T>() -> Result<T> {
}
#[async_trait]
impl Layer for Generated {
async fn stat(&self, path: &str) -> Result<Stat> {
async fn stat(&self, path: &str, _follow: bool) -> Result<Stat> {
if let Some(f) = self.files.get(path) {
return Snapshot(f()?).stat().await;
}
@@ -38,6 +39,7 @@ impl Layer for Generated {
}
Ok(Stat {
directory: true,
symlink: false,
size: 0,
accessed: None,
modified: None,
@@ -67,7 +69,11 @@ impl Layer for Generated {
}
Ok(entries
.into_iter()
.map(|(name, directory)| Entry { name, directory })
.map(|(name, directory)| Entry {
name,
directory,
symlink: false,
})
.collect())
}
async fn open(&self, path: &str, o: Open) -> Result<Arc<dyn File>> {
@@ -103,6 +109,7 @@ impl File for Snapshot {
async fn stat(&self) -> Result<Stat> {
Ok(Stat {
directory: false,
symlink: false,
size: self.0.len() as u64,
accessed: None,
modified: None,
+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);
}
}
+14 -2
View File
@@ -23,6 +23,7 @@ pub type Result<T> = io::Result<T>;
#[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>,
@@ -32,10 +33,13 @@ pub struct Stat {
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,
@@ -61,7 +65,11 @@ pub trait File: Send + Sync {
/// Implementations must retain open file identity and return portable IO errors.
#[async_trait]
pub trait Layer: Send + Sync {
async fn stat(&self, path: &str) -> Result<Stat>;
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<()>;
@@ -88,5 +96,9 @@ pub fn resolve(base: &str, path: &str) -> String {
p => parts.push(p),
}
}
format!("/{}", parts.join("/"))
let mut result = format!("/{}", parts.join("/"));
if path.ends_with('/') && result != "/" {
result.push('/');
}
result
}
+100 -33
View File
@@ -4,9 +4,10 @@ use std::{collections::BTreeMap, io::ErrorKind as E, sync::Mutex};
#[derive(Clone)]
enum Node {
Dir,
Dir(Arc<Mutex<Data>>),
File(Arc<Mutex<Data>>),
}
#[derive(Default)]
struct Data {
bytes: Vec<u8>,
accessed: Option<SystemTime>,
@@ -20,7 +21,10 @@ pub struct Memory {
impl Default for Memory {
fn default() -> Self {
Self {
nodes: Arc::new(Mutex::new(BTreeMap::from([("/".into(), Node::Dir)]))),
nodes: Arc::new(Mutex::new(BTreeMap::from([(
"/".into(),
Node::Dir(Arc::default()),
)]))),
}
}
}
@@ -29,25 +33,54 @@ fn parent(path: &str) -> &str {
.map(|(p, _)| if p.is_empty() { "/" } else { p })
.unwrap_or("/")
}
fn validate(nodes: &BTreeMap<String, Node>, path: &str) -> Result<()> {
let mut prefix = String::new();
let parts: Vec<_> = path
.trim_end_matches('/')
.split('/')
.filter(|p| !p.is_empty())
.collect();
for part in parts.iter().take(parts.len().saturating_sub(1)) {
prefix.push('/');
prefix.push_str(part);
directory(nodes, &prefix)?;
}
if path.ends_with('/') && path != "/" {
directory(nodes, path.trim_end_matches('/'))?;
}
Ok(())
}
fn clean(path: &str) -> &str {
if path == "/" {
path
} else {
path.trim_end_matches('/')
}
}
fn directory(nodes: &BTreeMap<String, Node>, path: &str) -> Result<()> {
match nodes.get(path) {
Some(Node::Dir) => Ok(()),
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::Dir(data) => {
let data = data.lock().unwrap();
Stat {
directory: true,
symlink: false,
size: 0,
accessed: data.accessed,
modified: data.modified,
}
}
Node::File(data) => {
let d = data.lock().unwrap();
Stat {
directory: false,
symlink: false,
size: d.bytes.len() as u64,
accessed: d.accessed,
modified: d.modified,
@@ -61,28 +94,34 @@ struct Handle {
}
#[async_trait]
impl Layer for Memory {
async fn stat(&self, path: &str) -> Result<Stat> {
self.nodes
.lock()
.unwrap()
.get(path)
async fn stat(&self, path: &str, follow: bool) -> Result<Stat> {
let _ = follow;
let nodes = self.nodes.lock().unwrap();
validate(&nodes, path)?;
nodes
.get(clean(path))
.map(metadata)
.ok_or_else(|| error(E::NotFound))
}
async fn list(&self, path: &str) -> Result<Vec<Entry>> {
let nodes = self.nodes.lock().unwrap();
validate(&nodes, path)?;
let path = clean(path);
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),
directory: matches!(n, Node::Dir(_)),
symlink: false,
})
.collect())
}
async fn open(&self, path: &str, o: Open) -> Result<Arc<dyn File>> {
let mut nodes = self.nodes.lock().unwrap();
validate(&nodes, path)?;
let path = clean(path);
if o.truncate && !o.write {
return Err(error(E::InvalidInput));
}
@@ -105,7 +144,7 @@ impl Layer for Memory {
}
let node = nodes[path].clone();
match &node {
Node::Dir if o.write || o.truncate => return Err(error(E::IsADirectory)),
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(),
_ => {}
@@ -117,18 +156,22 @@ impl Layer for Memory {
}
async fn mkdir(&self, path: &str) -> Result<()> {
let mut nodes = self.nodes.lock().unwrap();
validate(&nodes, path)?;
let path = clean(path);
if nodes.contains_key(path) {
return Err(error(E::AlreadyExists));
}
directory(&nodes, parent(path))?;
nodes.insert(path.into(), Node::Dir);
nodes.insert(path.into(), Node::Dir(Arc::default()));
Ok(())
}
async fn remove_file(&self, path: &str) -> Result<()> {
let mut nodes = self.nodes.lock().unwrap();
validate(&nodes, path)?;
let path = clean(path);
match nodes.get(path) {
None => return Err(error(E::NotFound)),
Some(Node::Dir) => return Err(error(E::IsADirectory)),
Some(Node::Dir(_)) => return Err(error(E::IsADirectory)),
_ => {}
}
nodes.remove(path);
@@ -136,6 +179,8 @@ impl Layer for Memory {
}
async fn remove_dir(&self, path: &str) -> Result<()> {
let mut nodes = self.nodes.lock().unwrap();
validate(&nodes, path)?;
let path = clean(path);
directory(&nodes, path)?;
if path == "/" {
return Err(error(E::ResourceBusy));
@@ -148,6 +193,10 @@ impl Layer for Memory {
}
async fn rename(&self, from: &str, to: &str) -> Result<()> {
let mut nodes = self.nodes.lock().unwrap();
validate(&nodes, from)?;
validate(&nodes, to)?;
let from = clean(from);
let to = clean(to);
let node = nodes.get(from).cloned().ok_or_else(|| error(E::NotFound))?;
directory(&nodes, parent(to))?;
if from == to {
@@ -161,9 +210,14 @@ impl Layer for Memory {
}
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) => {
(Node::Dir(_), Node::File(_)) => return Err(error(E::NotADirectory)),
(Node::File(_), Node::Dir(_)) if from.starts_with(&format!("{to}/")) => {
return Err(error(E::DirectoryNotEmpty));
}
(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));
}
_ => {}
@@ -210,31 +264,41 @@ impl File for Handle {
}
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 start = usize::try_from(offset).map_err(|_| error(E::FileTooLarge))?;
let end = start
.checked_add(bytes.len())
.ok_or_else(|| error(E::InvalidInput))?;
.ok_or_else(|| error(E::FileTooLarge))?;
if !bytes.is_empty() {
if end > d.bytes.len() {
let additional = end - d.bytes.len();
d.bytes
.try_reserve(additional)
.map_err(|_| error(E::FileTooLarge))?;
d.bytes.resize(end, 0);
}
d.bytes[start..end].copy_from_slice(&bytes);
d.modified = Some(SystemTime::now());
}
Ok(bytes.len())
}
async fn append(&self, bytes: Vec<u8>) -> Result<usize> {
self.writable()?
.lock()
.unwrap()
.bytes
.extend_from_slice(&bytes);
let mut d = self.writable()?.lock().unwrap();
d.bytes
.try_reserve(bytes.len())
.map_err(|_| error(E::FileTooLarge))?;
d.bytes.extend_from_slice(&bytes);
d.modified = Some(SystemTime::now());
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,
);
let mut d = self.writable()?.lock().unwrap();
let size = usize::try_from(size).map_err(|_| error(E::FileTooLarge))?;
let additional = size.saturating_sub(d.bytes.len());
d.bytes
.try_reserve(additional)
.map_err(|_| error(E::FileTooLarge))?;
d.bytes.resize(size, 0);
d.modified = Some(SystemTime::now());
Ok(())
}
async fn sync(&self) -> Result<()> {
@@ -245,7 +309,10 @@ impl File for Handle {
accessed: Option<SystemTime>,
modified: Option<SystemTime>,
) -> Result<()> {
let mut d = self.writable()?.lock().unwrap();
let data = match &self.node {
Node::Dir(d) | Node::File(d) => d,
};
let mut d = data.lock().unwrap();
if accessed.is_some() {
d.accessed = accessed;
}
+19 -2
View File
@@ -16,6 +16,18 @@ impl Mounts {
self.mounts
.insert(resolve("/", path), Mount { layer, readonly });
}
pub(crate) async fn metadata_file(&self, path: &str, follow: bool) -> Result<Arc<dyn File>> {
let (m, p) = self.route(path, true)?;
m.layer
.open(
&p,
Open {
follow,
..Open::default()
},
)
.await
}
fn route(&self, path: &str, write: bool) -> Result<(&Mount, String)> {
let path = resolve("/", path);
let (base, m) = self
@@ -37,9 +49,13 @@ impl Mounts {
}
#[async_trait]
impl Layer for Mounts {
async fn stat(&self, path: &str) -> Result<Stat> {
async fn stat(&self, path: &str, follow: bool) -> Result<Stat> {
let (m, p) = self.route(path, false)?;
m.layer.stat(&p).await
m.layer.stat(&p, follow).await
}
async fn readlink(&self, path: &str) -> Result<String> {
let (m, p) = self.route(path, false)?;
m.layer.readlink(&p).await
}
async fn list(&self, path: &str) -> Result<Vec<Entry>> {
let path = resolve("/", path);
@@ -59,6 +75,7 @@ impl Layer for Mounts {
entries.push(Entry {
name: name.into(),
directory: true,
symlink: false,
});
}
}
+4 -1
View File
@@ -36,7 +36,10 @@ impl Process {
.await
.map_err(std::io::Error::other)?
}
/// Wait for all outstanding system calls. The caller must prevent new operations.
/// Wait for all outstanding system calls.
/// First abort the guest task. Then await its JoinHandle, or drop its store.
/// Only then await idle before releasing locks. Abort alone does not stop a running poll.
/// This ordering also prevents later memory writes, which need no blocking guard.
pub async fn idle(&self) {
loop {
let notified = self.0.idle.notified();
+54 -4
View File
@@ -26,18 +26,20 @@ impl Reader {
error: None,
}
}
fn start(&mut self) {
fn start(&mut self, size: usize) {
if self.pending.is_none() && self.data.is_empty() && !self.eof {
let f = self.file.clone();
let offset = self.offset;
self.pending = Some(Box::pin(async move { f.read(offset, 65536).await }));
self.pending = Some(Box::pin(
async move { f.read(offset, size.min(65536)).await },
));
}
}
}
#[async_trait]
impl Pollable for Reader {
async fn ready(&mut self) {
self.start();
self.start(4096);
if let Some(task) = &mut self.pending {
match task.await {
Ok(data) => {
@@ -53,6 +55,14 @@ impl Pollable for Reader {
}
#[async_trait]
impl InputStream for Reader {
async fn blocking_read(&mut self, size: usize) -> StreamResult<Bytes> {
if size == 0 {
return Ok(Bytes::new());
}
self.start(size);
self.ready().await;
self.read(size)
}
fn read(&mut self, size: usize) -> StreamResult<Bytes> {
if let Some(e) = self.error.take() {
return Err(failure(e));
@@ -62,7 +72,9 @@ impl InputStream for Reader {
}
let n = size.min(self.data.len());
let data = self.data.split_to(n);
self.start();
if data.is_empty() && size != 0 {
self.start(size);
}
Ok(data)
}
}
@@ -100,6 +112,9 @@ impl Pollable for Writer {
}
#[async_trait]
impl OutputStream for Writer {
async fn cancel(&mut self) {
self.ready().await;
}
fn write(&mut self, bytes: Bytes) -> StreamResult<()> {
if self.check_write()? == 0 {
return Err(StreamError::trap("write without a permit"));
@@ -132,3 +147,38 @@ impl OutputStream for Writer {
Ok(if self.pending.is_some() { 0 } else { 65536 })
}
}
#[cfg(test)]
mod tests {
use super::*;
/// Both read APIs request only the bytes needed instead of caching later mutations.
#[tokio::test]
async fn small_reads() {
for blocking in [true, false] {
let memory = Memory::default();
let file = memory
.open(
"/file",
Open {
write: true,
create: true,
..Open::default()
},
)
.await
.unwrap();
file.write(0, b"abcd".to_vec()).await.unwrap();
let mut reader = Reader::new(file.clone(), 0);
let first = if blocking {
reader.blocking_read(1).await.unwrap()
} else {
assert!(reader.read(1).unwrap().is_empty());
reader.ready().await;
reader.read(1).unwrap()
};
assert_eq!(first.as_ref(), b"a");
file.write(1, b"Z".to_vec()).await.unwrap();
assert_eq!(reader.blocking_read(1).await.unwrap().as_ref(), b"Z");
}
}
}