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:
@@ -0,0 +1,470 @@
|
||||
//! Wasmtime 49 filesystem interfaces backed exclusively by the virtual mount namespace.
|
||||
use crate::streams::{Reader, Writer};
|
||||
use crate::*;
|
||||
use wasmtime::component::{HasData, Linker, Resource, ResourceTable};
|
||||
use wasmtime_wasi::p2::{
|
||||
DynInputStream, DynOutputStream, FsError, FsResult,
|
||||
bindings::filesystem::{
|
||||
preopens,
|
||||
types::{
|
||||
self, DescriptorFlags, DescriptorType, DirectoryEntry, ErrorCode, HostDescriptor,
|
||||
HostDirectoryEntryStream, OpenFlags,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/// Store-owned filesystem namespace.
|
||||
pub struct Adapter {
|
||||
pub mounts: Arc<Mounts>,
|
||||
}
|
||||
impl Adapter {
|
||||
pub fn new(mounts: Mounts) -> Self {
|
||||
Self {
|
||||
mounts: Arc::new(mounts),
|
||||
}
|
||||
}
|
||||
}
|
||||
/// The filesystem and shared WASI resource table borrowed from store state.
|
||||
pub struct View<'a> {
|
||||
pub adapter: &'a mut Adapter,
|
||||
pub table: &'a mut ResourceTable,
|
||||
}
|
||||
pub struct Vfs;
|
||||
impl HasData for Vfs {
|
||||
type Data<'a> = View<'a>;
|
||||
}
|
||||
/// Replace filesystem imports after adding the standard asynchronous WASI linker interfaces.
|
||||
pub fn add_to_linker<T: Send + 'static>(
|
||||
linker: &mut Linker<T>,
|
||||
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)?;
|
||||
Ok(())
|
||||
}
|
||||
#[derive(Clone)]
|
||||
struct Descriptor {
|
||||
path: String,
|
||||
file: Arc<dyn File>,
|
||||
flags: DescriptorFlags,
|
||||
}
|
||||
struct Entries(std::vec::IntoIter<DirectoryEntry>);
|
||||
fn code(e: &io::Error) -> ErrorCode {
|
||||
use io::ErrorKind::*;
|
||||
match e.kind() {
|
||||
NotADirectory => ErrorCode::NotDirectory,
|
||||
IsADirectory => ErrorCode::IsDirectory,
|
||||
DirectoryNotEmpty => ErrorCode::NotEmpty,
|
||||
ReadOnlyFilesystem => ErrorCode::ReadOnly,
|
||||
CrossesDevices => ErrorCode::CrossDevice,
|
||||
Unsupported => ErrorCode::Unsupported,
|
||||
_ => ErrorCode::from(e),
|
||||
}
|
||||
}
|
||||
fn errno(e: io::Error) -> FsError {
|
||||
code(&e).into()
|
||||
}
|
||||
fn stat(s: Stat) -> types::DescriptorStat {
|
||||
fn timestamp(t: SystemTime) -> Option<types::Datetime> {
|
||||
let d = t.duration_since(SystemTime::UNIX_EPOCH).ok()?;
|
||||
Some(types::Datetime {
|
||||
seconds: d.as_secs(),
|
||||
nanoseconds: d.subsec_nanos(),
|
||||
})
|
||||
}
|
||||
types::DescriptorStat {
|
||||
type_: if s.directory {
|
||||
types::DescriptorType::Directory
|
||||
} else {
|
||||
types::DescriptorType::RegularFile
|
||||
},
|
||||
link_count: 1,
|
||||
size: s.size,
|
||||
data_access_timestamp: s.accessed.and_then(timestamp),
|
||||
data_modification_timestamp: s.modified.and_then(timestamp),
|
||||
status_change_timestamp: None,
|
||||
}
|
||||
}
|
||||
fn time(t: types::NewTimestamp) -> Option<SystemTime> {
|
||||
match t {
|
||||
types::NewTimestamp::NoChange => None,
|
||||
types::NewTimestamp::Now => Some(SystemTime::now()),
|
||||
types::NewTimestamp::Timestamp(t) => {
|
||||
Some(SystemTime::UNIX_EPOCH + std::time::Duration::new(t.seconds, t.nanoseconds))
|
||||
}
|
||||
}
|
||||
}
|
||||
fn hash(path: &str) -> types::MetadataHashValue {
|
||||
use std::hash::{Hash, Hasher};
|
||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||
path.hash(&mut h);
|
||||
types::MetadataHashValue {
|
||||
lower: h.finish(),
|
||||
upper: 0,
|
||||
}
|
||||
}
|
||||
impl View<'_> {
|
||||
fn fd(&self, fd: &Resource<types::Descriptor>) -> FsResult<Descriptor> {
|
||||
Ok(self
|
||||
.table
|
||||
.get(&Resource::<Descriptor>::new_borrow(fd.rep()))?
|
||||
.clone())
|
||||
}
|
||||
fn path(&self, fd: &Resource<types::Descriptor>, path: &str) -> FsResult<String> {
|
||||
Ok(resolve(&self.fd(fd)?.path, path))
|
||||
}
|
||||
fn push(&mut self, descriptor: Descriptor) -> FsResult<Resource<types::Descriptor>> {
|
||||
Ok(Resource::new_own(self.table.push(descriptor)?.rep()))
|
||||
}
|
||||
}
|
||||
impl preopens::Host for View<'_> {
|
||||
fn get_directories(&mut self) -> wasmtime::Result<Vec<(Resource<types::Descriptor>, String)>> {
|
||||
// The root descriptor needs no IO: directory operations use its virtual path.
|
||||
let file: Arc<dyn File> = Arc::new(Root);
|
||||
let fd = self.push(Descriptor {
|
||||
path: "/".into(),
|
||||
file,
|
||||
flags: DescriptorFlags::READ | DescriptorFlags::MUTATE_DIRECTORY,
|
||||
})?;
|
||||
Ok(vec![(fd, "/".into())])
|
||||
}
|
||||
}
|
||||
struct Root;
|
||||
#[async_trait]
|
||||
impl File for Root {
|
||||
async fn stat(&self) -> Result<Stat> {
|
||||
Ok(Stat {
|
||||
directory: true,
|
||||
size: 0,
|
||||
accessed: None,
|
||||
modified: None,
|
||||
})
|
||||
}
|
||||
async fn read(&self, _: u64, _: usize) -> Result<Vec<u8>> {
|
||||
Err(error(io::ErrorKind::IsADirectory))
|
||||
}
|
||||
async fn write(&self, _: u64, _: Vec<u8>) -> Result<usize> {
|
||||
Err(error(io::ErrorKind::IsADirectory))
|
||||
}
|
||||
async fn append(&self, _: Vec<u8>) -> Result<usize> {
|
||||
Err(error(io::ErrorKind::IsADirectory))
|
||||
}
|
||||
async fn set_size(&self, _: u64) -> Result<()> {
|
||||
Err(error(io::ErrorKind::IsADirectory))
|
||||
}
|
||||
async fn sync(&self) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
async fn set_times(&self, _: Option<SystemTime>, _: Option<SystemTime>) -> Result<()> {
|
||||
Err(error(io::ErrorKind::Unsupported))
|
||||
}
|
||||
}
|
||||
impl types::Host for View<'_> {
|
||||
fn convert_error_code(&mut self, err: FsError) -> wasmtime::Result<ErrorCode> {
|
||||
err.downcast()
|
||||
}
|
||||
fn filesystem_error_code(
|
||||
&mut self,
|
||||
err: Resource<wasmtime::Error>,
|
||||
) -> wasmtime::Result<Option<ErrorCode>> {
|
||||
Ok(self.table.get(&err)?.downcast_ref::<io::Error>().map(code))
|
||||
}
|
||||
}
|
||||
|
||||
impl HostDescriptor for View<'_> {
|
||||
async fn advise(
|
||||
&mut self,
|
||||
fd: Resource<types::Descriptor>,
|
||||
offset: types::Filesize,
|
||||
len: types::Filesize,
|
||||
advice: types::Advice,
|
||||
) -> FsResult<()> {
|
||||
let _ = (fd, offset, len, advice);
|
||||
Ok(())
|
||||
}
|
||||
async fn sync_data(&mut self, fd: Resource<types::Descriptor>) -> FsResult<()> {
|
||||
self.fd(&fd)?.file.sync().await.map_err(errno)
|
||||
}
|
||||
async fn get_flags(&mut self, fd: Resource<types::Descriptor>) -> FsResult<DescriptorFlags> {
|
||||
Ok(self.fd(&fd)?.flags)
|
||||
}
|
||||
async fn get_type(&mut self, fd: Resource<types::Descriptor>) -> FsResult<DescriptorType> {
|
||||
Ok(stat(self.fd(&fd)?.file.stat().await.map_err(errno)?).type_)
|
||||
}
|
||||
async fn set_size(
|
||||
&mut self,
|
||||
fd: Resource<types::Descriptor>,
|
||||
size: types::Filesize,
|
||||
) -> FsResult<()> {
|
||||
self.fd(&fd)?.file.set_size(size).await.map_err(errno)
|
||||
}
|
||||
async fn set_times(
|
||||
&mut self,
|
||||
fd: Resource<types::Descriptor>,
|
||||
atim: types::NewTimestamp,
|
||||
mtim: types::NewTimestamp,
|
||||
) -> FsResult<()> {
|
||||
self.fd(&fd)?
|
||||
.file
|
||||
.set_times(time(atim), time(mtim))
|
||||
.await
|
||||
.map_err(errno)
|
||||
}
|
||||
async fn read(
|
||||
&mut self,
|
||||
fd: Resource<types::Descriptor>,
|
||||
len: types::Filesize,
|
||||
offset: types::Filesize,
|
||||
) -> FsResult<(Vec<u8>, bool)> {
|
||||
let bytes = self
|
||||
.fd(&fd)?
|
||||
.file
|
||||
.read(offset, len as usize)
|
||||
.await
|
||||
.map_err(errno)?;
|
||||
let eof = bytes.is_empty();
|
||||
Ok((bytes, eof))
|
||||
}
|
||||
async fn write(
|
||||
&mut self,
|
||||
fd: Resource<types::Descriptor>,
|
||||
buf: Vec<u8>,
|
||||
offset: types::Filesize,
|
||||
) -> FsResult<types::Filesize> {
|
||||
self.fd(&fd)?
|
||||
.file
|
||||
.write(offset, buf)
|
||||
.await
|
||||
.map(|n| n as u64)
|
||||
.map_err(errno)
|
||||
}
|
||||
async fn read_directory(
|
||||
&mut self,
|
||||
fd: Resource<types::Descriptor>,
|
||||
) -> FsResult<Resource<types::DirectoryEntryStream>> {
|
||||
let d = self.fd(&fd)?;
|
||||
let entries = self
|
||||
.adapter
|
||||
.mounts
|
||||
.list(&d.path)
|
||||
.await
|
||||
.map_err(errno)?
|
||||
.into_iter()
|
||||
.map(|e| DirectoryEntry {
|
||||
name: e.name,
|
||||
type_: if e.directory {
|
||||
types::DescriptorType::Directory
|
||||
} else {
|
||||
types::DescriptorType::RegularFile
|
||||
},
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
Ok(Resource::new_own(
|
||||
self.table.push(Entries(entries.into_iter()))?.rep(),
|
||||
))
|
||||
}
|
||||
async fn sync(&mut self, fd: Resource<types::Descriptor>) -> FsResult<()> {
|
||||
self.fd(&fd)?.file.sync().await.map_err(errno)
|
||||
}
|
||||
async fn create_directory_at(
|
||||
&mut self,
|
||||
fd: Resource<types::Descriptor>,
|
||||
path: String,
|
||||
) -> FsResult<()> {
|
||||
let p = self.path(&fd, &path)?;
|
||||
self.adapter.mounts.mkdir(&p).await.map_err(errno)
|
||||
}
|
||||
async fn stat(&mut self, fd: Resource<types::Descriptor>) -> FsResult<types::DescriptorStat> {
|
||||
Ok(stat(self.fd(&fd)?.file.stat().await.map_err(errno)?))
|
||||
}
|
||||
async fn stat_at(
|
||||
&mut self,
|
||||
fd: Resource<types::Descriptor>,
|
||||
path_flags: types::PathFlags,
|
||||
path: String,
|
||||
) -> FsResult<types::DescriptorStat> {
|
||||
let _ = path_flags;
|
||||
let p = self.path(&fd, &path)?;
|
||||
Ok(stat(self.adapter.mounts.stat(&p).await.map_err(errno)?))
|
||||
}
|
||||
async fn set_times_at(
|
||||
&mut self,
|
||||
fd: Resource<types::Descriptor>,
|
||||
path_flags: types::PathFlags,
|
||||
path: String,
|
||||
atim: types::NewTimestamp,
|
||||
mtim: types::NewTimestamp,
|
||||
) -> FsResult<()> {
|
||||
let _ = path_flags;
|
||||
let p = self.path(&fd, &path)?;
|
||||
self.adapter
|
||||
.mounts
|
||||
.open(
|
||||
&p,
|
||||
Open {
|
||||
write: true,
|
||||
..Open::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(errno)?
|
||||
.set_times(time(atim), time(mtim))
|
||||
.await
|
||||
.map_err(errno)
|
||||
}
|
||||
async fn link_at(
|
||||
&mut self,
|
||||
fd: Resource<types::Descriptor>,
|
||||
old_path_flags: types::PathFlags,
|
||||
old_path: String,
|
||||
new_descriptor: Resource<types::Descriptor>,
|
||||
new_path: String,
|
||||
) -> FsResult<()> {
|
||||
let _ = (fd, old_path_flags, old_path, new_descriptor, new_path);
|
||||
Err(ErrorCode::Unsupported.into())
|
||||
}
|
||||
async fn open_at(
|
||||
&mut self,
|
||||
fd: Resource<types::Descriptor>,
|
||||
path_flags: types::PathFlags,
|
||||
path: String,
|
||||
oflags: OpenFlags,
|
||||
flags: DescriptorFlags,
|
||||
) -> FsResult<Resource<types::Descriptor>> {
|
||||
let _ = path_flags;
|
||||
let path = self.path(&fd, &path)?;
|
||||
let file = self
|
||||
.adapter
|
||||
.mounts
|
||||
.open(
|
||||
&path,
|
||||
Open {
|
||||
write: flags.contains(DescriptorFlags::WRITE),
|
||||
create: oflags.contains(OpenFlags::CREATE),
|
||||
exclusive: oflags.contains(OpenFlags::EXCLUSIVE),
|
||||
truncate: oflags.contains(OpenFlags::TRUNCATE),
|
||||
directory: oflags.contains(OpenFlags::DIRECTORY),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(errno)?;
|
||||
self.push(Descriptor { path, file, flags })
|
||||
}
|
||||
fn drop(&mut self, fd: Resource<types::Descriptor>) -> wasmtime::Result<()> {
|
||||
self.table
|
||||
.delete(Resource::<Descriptor>::new_own(fd.rep()))?;
|
||||
Ok(())
|
||||
}
|
||||
async fn readlink_at(
|
||||
&mut self,
|
||||
fd: Resource<types::Descriptor>,
|
||||
path: String,
|
||||
) -> FsResult<String> {
|
||||
let _ = (fd, path);
|
||||
Err(ErrorCode::Unsupported.into())
|
||||
}
|
||||
async fn remove_directory_at(
|
||||
&mut self,
|
||||
fd: Resource<types::Descriptor>,
|
||||
path: String,
|
||||
) -> FsResult<()> {
|
||||
let p = self.path(&fd, &path)?;
|
||||
self.adapter.mounts.remove_dir(&p).await.map_err(errno)
|
||||
}
|
||||
async fn rename_at(
|
||||
&mut self,
|
||||
fd: Resource<types::Descriptor>,
|
||||
old_path: String,
|
||||
new_fd: Resource<types::Descriptor>,
|
||||
new_path: String,
|
||||
) -> FsResult<()> {
|
||||
let a = self.path(&fd, &old_path)?;
|
||||
let b = self.path(&new_fd, &new_path)?;
|
||||
self.adapter.mounts.rename(&a, &b).await.map_err(errno)
|
||||
}
|
||||
async fn symlink_at(
|
||||
&mut self,
|
||||
fd: Resource<types::Descriptor>,
|
||||
src_path: String,
|
||||
dest_path: String,
|
||||
) -> FsResult<()> {
|
||||
let _ = (fd, src_path, dest_path);
|
||||
Err(ErrorCode::Unsupported.into())
|
||||
}
|
||||
async fn unlink_file_at(
|
||||
&mut self,
|
||||
fd: Resource<types::Descriptor>,
|
||||
path: String,
|
||||
) -> FsResult<()> {
|
||||
let p = self.path(&fd, &path)?;
|
||||
self.adapter.mounts.remove_file(&p).await.map_err(errno)
|
||||
}
|
||||
fn read_via_stream(
|
||||
&mut self,
|
||||
fd: Resource<types::Descriptor>,
|
||||
offset: types::Filesize,
|
||||
) -> FsResult<Resource<DynInputStream>> {
|
||||
let d = self.fd(&fd)?;
|
||||
let stream: DynInputStream = Box::new(Reader::new(d.file, offset));
|
||||
Ok(self.table.push(stream)?)
|
||||
}
|
||||
fn write_via_stream(
|
||||
&mut self,
|
||||
fd: Resource<types::Descriptor>,
|
||||
offset: types::Filesize,
|
||||
) -> FsResult<Resource<DynOutputStream>> {
|
||||
let d = self.fd(&fd)?;
|
||||
let stream: DynOutputStream = Box::new(Writer::new(d.file, Some(offset)));
|
||||
Ok(self.table.push(stream)?)
|
||||
}
|
||||
fn append_via_stream(
|
||||
&mut self,
|
||||
fd: Resource<types::Descriptor>,
|
||||
) -> FsResult<Resource<DynOutputStream>> {
|
||||
let d = self.fd(&fd)?;
|
||||
let stream: DynOutputStream = Box::new(Writer::new(d.file, None));
|
||||
Ok(self.table.push(stream)?)
|
||||
}
|
||||
async fn is_same_object(
|
||||
&mut self,
|
||||
a: Resource<types::Descriptor>,
|
||||
b: Resource<types::Descriptor>,
|
||||
) -> wasmtime::Result<bool> {
|
||||
Ok(self.fd(&a)?.path == self.fd(&b)?.path)
|
||||
}
|
||||
async fn metadata_hash(
|
||||
&mut self,
|
||||
fd: Resource<types::Descriptor>,
|
||||
) -> FsResult<types::MetadataHashValue> {
|
||||
Ok(hash(&self.fd(&fd)?.path))
|
||||
}
|
||||
async fn metadata_hash_at(
|
||||
&mut self,
|
||||
fd: Resource<types::Descriptor>,
|
||||
path_flags: types::PathFlags,
|
||||
path: String,
|
||||
) -> FsResult<types::MetadataHashValue> {
|
||||
let _ = path_flags;
|
||||
let p = self.path(&fd, &path)?;
|
||||
self.adapter.mounts.stat(&p).await.map_err(errno)?;
|
||||
Ok(hash(&p))
|
||||
}
|
||||
}
|
||||
impl HostDirectoryEntryStream for View<'_> {
|
||||
async fn read_directory_entry(
|
||||
&mut self,
|
||||
stream: Resource<types::DirectoryEntryStream>,
|
||||
) -> FsResult<Option<DirectoryEntry>> {
|
||||
Ok(self
|
||||
.table
|
||||
.get_mut(&Resource::<Entries>::new_borrow(stream.rep()))?
|
||||
.0
|
||||
.next())
|
||||
}
|
||||
fn drop(&mut self, stream: Resource<types::DirectoryEntryStream>) -> wasmtime::Result<()> {
|
||||
self.table
|
||||
.delete(Resource::<Entries>::new_own(stream.rep()))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
//! Read-only generated files. Each open captures an independent finite snapshot.
|
||||
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.
|
||||
#[derive(Default)]
|
||||
pub struct Generated {
|
||||
files: BTreeMap<String, Generator>,
|
||||
}
|
||||
impl Generated {
|
||||
pub fn insert(
|
||||
&mut self,
|
||||
path: &str,
|
||||
generate: impl Fn() -> Result<Vec<u8>> + Send + Sync + 'static,
|
||||
) {
|
||||
self.files.insert(resolve("/", path), Arc::new(generate));
|
||||
}
|
||||
fn directory(&self, path: &str) -> bool {
|
||||
path == "/"
|
||||
|| self
|
||||
.files
|
||||
.keys()
|
||||
.any(|p| p.starts_with(&format!("{path}/")))
|
||||
}
|
||||
}
|
||||
struct Snapshot(Vec<u8>);
|
||||
fn readonly<T>() -> Result<T> {
|
||||
Err(error(E::ReadOnlyFilesystem))
|
||||
}
|
||||
#[async_trait]
|
||||
impl Layer for Generated {
|
||||
async fn stat(&self, path: &str) -> Result<Stat> {
|
||||
if let Some(f) = self.files.get(path) {
|
||||
return Snapshot(f()?).stat().await;
|
||||
}
|
||||
if !self.directory(path) {
|
||||
return Err(error(E::NotFound));
|
||||
}
|
||||
Ok(Stat {
|
||||
directory: true,
|
||||
size: 0,
|
||||
accessed: None,
|
||||
modified: None,
|
||||
})
|
||||
}
|
||||
async fn list(&self, path: &str) -> Result<Vec<Entry>> {
|
||||
if !self.directory(path) {
|
||||
return Err(error(if self.files.contains_key(path) {
|
||||
E::NotADirectory
|
||||
} else {
|
||||
E::NotFound
|
||||
}));
|
||||
}
|
||||
let prefix = if path == "/" {
|
||||
"/".into()
|
||||
} else {
|
||||
format!("{path}/")
|
||||
};
|
||||
let mut entries = BTreeMap::new();
|
||||
for p in self.files.keys() {
|
||||
if let Some(rest) = p.strip_prefix(&prefix) {
|
||||
let (name, directory) = rest
|
||||
.split_once('/')
|
||||
.map_or((rest, false), |(name, _)| (name, true));
|
||||
entries.insert(name.to_string(), directory);
|
||||
}
|
||||
}
|
||||
Ok(entries
|
||||
.into_iter()
|
||||
.map(|(name, directory)| Entry { name, directory })
|
||||
.collect())
|
||||
}
|
||||
async fn open(&self, path: &str, o: Open) -> Result<Arc<dyn File>> {
|
||||
if o.write || o.create || o.truncate {
|
||||
return readonly();
|
||||
}
|
||||
if let Some(f) = self.files.get(path) {
|
||||
if o.directory {
|
||||
return Err(error(E::NotADirectory));
|
||||
}
|
||||
return Ok(Arc::new(Snapshot(f()?)));
|
||||
}
|
||||
if self.directory(path) {
|
||||
return Memory::default().open("/", o).await;
|
||||
}
|
||||
Err(error(E::NotFound))
|
||||
}
|
||||
async fn mkdir(&self, _: &str) -> Result<()> {
|
||||
readonly()
|
||||
}
|
||||
async fn remove_file(&self, _: &str) -> Result<()> {
|
||||
readonly()
|
||||
}
|
||||
async fn remove_dir(&self, _: &str) -> Result<()> {
|
||||
readonly()
|
||||
}
|
||||
async fn rename(&self, _: &str, _: &str) -> Result<()> {
|
||||
readonly()
|
||||
}
|
||||
}
|
||||
#[async_trait]
|
||||
impl File for Snapshot {
|
||||
async fn stat(&self) -> Result<Stat> {
|
||||
Ok(Stat {
|
||||
directory: false,
|
||||
size: self.0.len() as u64,
|
||||
accessed: None,
|
||||
modified: None,
|
||||
})
|
||||
}
|
||||
async fn read(&self, offset: u64, length: usize) -> Result<Vec<u8>> {
|
||||
let start = usize::try_from(offset)
|
||||
.unwrap_or(usize::MAX)
|
||||
.min(self.0.len());
|
||||
Ok(self.0[start..start.saturating_add(length).min(self.0.len())].to_vec())
|
||||
}
|
||||
async fn write(&self, _: u64, _: Vec<u8>) -> Result<usize> {
|
||||
readonly()
|
||||
}
|
||||
async fn append(&self, _: Vec<u8>) -> Result<usize> {
|
||||
readonly()
|
||||
}
|
||||
async fn set_size(&self, _: u64) -> Result<()> {
|
||||
readonly()
|
||||
}
|
||||
async fn sync(&self) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
async fn set_times(&self, _: Option<SystemTime>, _: Option<SystemTime>) -> Result<()> {
|
||||
readonly()
|
||||
}
|
||||
}
|
||||
+190
@@ -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
|
||||
}
|
||||
}
|
||||
+92
-1
@@ -1 +1,92 @@
|
||||
//! A composable virtual filesystem for WASI guests in Wasmtime.
|
||||
//! Composable filesystem layers, open files, mount routing, and process IO guards.
|
||||
|
||||
pub mod adapter;
|
||||
pub mod generated;
|
||||
pub mod host;
|
||||
pub mod memory;
|
||||
pub mod mounts;
|
||||
pub mod process;
|
||||
mod readonly;
|
||||
mod streams;
|
||||
|
||||
pub use async_trait::async_trait;
|
||||
pub use generated::Generated;
|
||||
pub use host::Host;
|
||||
pub use memory::Memory;
|
||||
pub use mounts::Mounts;
|
||||
pub use process::Process;
|
||||
use std::{io, sync::Arc, time::SystemTime};
|
||||
|
||||
/// A filesystem result. Error kinds preserve portable filesystem failures.
|
||||
pub type Result<T> = io::Result<T>;
|
||||
/// The type and timestamps of a node.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Stat {
|
||||
pub directory: bool,
|
||||
pub size: u64,
|
||||
pub accessed: Option<SystemTime>,
|
||||
pub modified: Option<SystemTime>,
|
||||
}
|
||||
/// One child of a folder.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Entry {
|
||||
pub name: String,
|
||||
pub directory: bool,
|
||||
}
|
||||
/// Options for an open operation. Exclusive applies only with create.
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct Open {
|
||||
pub write: bool,
|
||||
pub create: bool,
|
||||
pub exclusive: bool,
|
||||
pub truncate: bool,
|
||||
pub directory: bool,
|
||||
}
|
||||
/// An open file keeps its identity after rename or unlink.
|
||||
#[async_trait]
|
||||
pub trait File: Send + Sync {
|
||||
async fn stat(&self) -> Result<Stat>;
|
||||
async fn read(&self, offset: u64, length: usize) -> Result<Vec<u8>>;
|
||||
async fn write(&self, offset: u64, bytes: Vec<u8>) -> Result<usize>;
|
||||
async fn append(&self, bytes: Vec<u8>) -> Result<usize>;
|
||||
async fn set_size(&self, size: u64) -> Result<()>;
|
||||
async fn sync(&self) -> Result<()>;
|
||||
async fn set_times(
|
||||
&self,
|
||||
accessed: Option<SystemTime>,
|
||||
modified: Option<SystemTime>,
|
||||
) -> Result<()>;
|
||||
}
|
||||
/// One filesystem layer. Paths are normalized, absolute, and relative to its root.
|
||||
/// 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 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<()>;
|
||||
async fn remove_file(&self, path: &str) -> Result<()>;
|
||||
async fn remove_dir(&self, path: &str) -> Result<()>;
|
||||
async fn rename(&self, from: &str, to: &str) -> Result<()>;
|
||||
}
|
||||
|
||||
pub(crate) fn error(kind: io::ErrorKind) -> io::Error {
|
||||
kind.into()
|
||||
}
|
||||
/// Resolve a path in the virtual namespace, including traversal across mounts.
|
||||
pub fn resolve(base: &str, path: &str) -> String {
|
||||
let mut parts = Vec::new();
|
||||
if !path.starts_with('/') {
|
||||
parts.extend(base.split('/').filter(|s| !s.is_empty()));
|
||||
}
|
||||
for part in path.split('/') {
|
||||
match part {
|
||||
"" | "." => {}
|
||||
".." => {
|
||||
parts.pop();
|
||||
}
|
||||
p => parts.push(p),
|
||||
}
|
||||
}
|
||||
format!("/{}", parts.join("/"))
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
//! Resolves virtual paths before routing them to mounted layers.
|
||||
use crate::*;
|
||||
use std::{collections::BTreeMap, io::ErrorKind as E};
|
||||
struct Mount {
|
||||
layer: Arc<dyn Layer>,
|
||||
readonly: bool,
|
||||
}
|
||||
/// A per-process mount namespace. Mount the root before use.
|
||||
#[derive(Default)]
|
||||
pub struct Mounts {
|
||||
mounts: BTreeMap<String, Mount>,
|
||||
}
|
||||
impl Mounts {
|
||||
/// Add or replace a mount. Read-only mounts reject every mutation.
|
||||
pub fn mount(&mut self, path: &str, layer: Arc<dyn Layer>, readonly: bool) {
|
||||
self.mounts
|
||||
.insert(resolve("/", path), Mount { layer, readonly });
|
||||
}
|
||||
fn route(&self, path: &str, write: bool) -> Result<(&Mount, String)> {
|
||||
let path = resolve("/", path);
|
||||
let (base, m) = self
|
||||
.mounts
|
||||
.iter()
|
||||
.filter(|(p, _)| p.as_str() == "/" || path == **p || path.starts_with(&format!("{p}/")))
|
||||
.max_by_key(|(p, _)| p.len())
|
||||
.ok_or_else(|| error(E::NotFound))?;
|
||||
if write && m.readonly {
|
||||
return Err(error(E::ReadOnlyFilesystem));
|
||||
}
|
||||
let rest = if base == "/" {
|
||||
path
|
||||
} else {
|
||||
path[base.len()..].into()
|
||||
};
|
||||
Ok((m, if rest.is_empty() { "/".into() } else { rest }))
|
||||
}
|
||||
}
|
||||
#[async_trait]
|
||||
impl Layer for Mounts {
|
||||
async fn stat(&self, path: &str) -> Result<Stat> {
|
||||
let (m, p) = self.route(path, false)?;
|
||||
m.layer.stat(&p).await
|
||||
}
|
||||
async fn list(&self, path: &str) -> Result<Vec<Entry>> {
|
||||
let path = resolve("/", path);
|
||||
let (m, p) = self.route(&path, false)?;
|
||||
let mut entries = m.layer.list(&p).await?;
|
||||
let prefix = if path == "/" {
|
||||
"/".into()
|
||||
} else {
|
||||
format!("{path}/")
|
||||
};
|
||||
for mount in self.mounts.keys() {
|
||||
if let Some(rest) = mount.strip_prefix(&prefix)
|
||||
&& !rest.is_empty()
|
||||
{
|
||||
let name = rest.split('/').next().unwrap();
|
||||
entries.retain(|e| e.name != name);
|
||||
entries.push(Entry {
|
||||
name: name.into(),
|
||||
directory: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
entries.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
Ok(entries)
|
||||
}
|
||||
async fn open(&self, path: &str, options: Open) -> Result<Arc<dyn File>> {
|
||||
let (m, p) = self.route(path, options.write || options.create || options.truncate)?;
|
||||
let file = m.layer.open(&p, options).await?;
|
||||
if m.readonly {
|
||||
Ok(Arc::new(crate::readonly::Readonly(file)))
|
||||
} else {
|
||||
Ok(file)
|
||||
}
|
||||
}
|
||||
async fn mkdir(&self, path: &str) -> Result<()> {
|
||||
let (m, p) = self.route(path, true)?;
|
||||
m.layer.mkdir(&p).await
|
||||
}
|
||||
async fn remove_file(&self, path: &str) -> Result<()> {
|
||||
let (m, p) = self.route(path, true)?;
|
||||
m.layer.remove_file(&p).await
|
||||
}
|
||||
async fn remove_dir(&self, path: &str) -> Result<()> {
|
||||
let (m, p) = self.route(path, true)?;
|
||||
m.layer.remove_dir(&p).await
|
||||
}
|
||||
async fn rename(&self, from: &str, to: &str) -> Result<()> {
|
||||
let (a, p) = self.route(from, true)?;
|
||||
let (b, q) = self.route(to, true)?;
|
||||
if !std::ptr::eq(a, b) {
|
||||
return Err(error(E::CrossesDevices));
|
||||
}
|
||||
a.layer.rename(&p, &q).await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
//! Tracks blocking operations until their system calls return, independent of cancellation.
|
||||
use std::sync::{
|
||||
Arc,
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
};
|
||||
use tokio::sync::Notify;
|
||||
|
||||
#[derive(Default)]
|
||||
struct State {
|
||||
count: AtomicUsize,
|
||||
idle: Notify,
|
||||
}
|
||||
/// A per-process operation tracker. Stop the process before awaiting idle.
|
||||
#[derive(Clone, Default)]
|
||||
pub struct Process(Arc<State>);
|
||||
struct Guard(Arc<State>);
|
||||
impl Drop for Guard {
|
||||
fn drop(&mut self) {
|
||||
if self.0.count.fetch_sub(1, Ordering::SeqCst) == 1 {
|
||||
self.0.idle.notify_waiters();
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Process {
|
||||
/// Run a system call on the blocking pool. Cancellation does not release its guard.
|
||||
pub async fn blocking<T: Send + 'static>(
|
||||
&self,
|
||||
operation: impl FnOnce() -> std::io::Result<T> + Send + 'static,
|
||||
) -> std::io::Result<T> {
|
||||
self.0.count.fetch_add(1, Ordering::SeqCst);
|
||||
let guard = Guard(self.0.clone());
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let _guard = guard;
|
||||
operation()
|
||||
})
|
||||
.await
|
||||
.map_err(std::io::Error::other)?
|
||||
}
|
||||
/// Wait for all outstanding system calls. The caller must prevent new operations.
|
||||
pub async fn idle(&self) {
|
||||
loop {
|
||||
let notified = self.0.idle.notified();
|
||||
tokio::pin!(notified);
|
||||
notified.as_mut().enable();
|
||||
if self.0.count.load(Ordering::SeqCst) == 0 {
|
||||
return;
|
||||
}
|
||||
notified.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
//! Preserves read-only mount errors for mutations through open descriptors.
|
||||
use crate::*;
|
||||
pub(crate) struct Readonly(pub Arc<dyn File>);
|
||||
fn denied<T>() -> Result<T> {
|
||||
Err(error(io::ErrorKind::ReadOnlyFilesystem))
|
||||
}
|
||||
#[async_trait]
|
||||
impl File for Readonly {
|
||||
async fn stat(&self) -> Result<Stat> {
|
||||
self.0.stat().await
|
||||
}
|
||||
async fn read(&self, offset: u64, length: usize) -> Result<Vec<u8>> {
|
||||
self.0.read(offset, length).await
|
||||
}
|
||||
async fn write(&self, _: u64, _: Vec<u8>) -> Result<usize> {
|
||||
denied()
|
||||
}
|
||||
async fn append(&self, _: Vec<u8>) -> Result<usize> {
|
||||
denied()
|
||||
}
|
||||
async fn set_size(&self, _: u64) -> Result<()> {
|
||||
denied()
|
||||
}
|
||||
async fn sync(&self) -> Result<()> {
|
||||
self.0.sync().await
|
||||
}
|
||||
async fn set_times(&self, _: Option<SystemTime>, _: Option<SystemTime>) -> Result<()> {
|
||||
denied()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
//! Nonblocking WASI streams that await layer operations through readiness.
|
||||
use crate::*;
|
||||
use bytes::Bytes;
|
||||
use std::{future::Future, pin::Pin};
|
||||
type Pending<T> = Pin<Box<dyn Future<Output = Result<T>> + Send>>;
|
||||
use wasmtime_wasi::p2::{InputStream, OutputStream, Pollable, StreamError, StreamResult};
|
||||
fn failure(e: io::Error) -> StreamError {
|
||||
StreamError::LastOperationFailed(e.into())
|
||||
}
|
||||
pub(crate) struct Reader {
|
||||
file: Arc<dyn File>,
|
||||
offset: u64,
|
||||
pending: Option<Pending<Vec<u8>>>,
|
||||
data: Bytes,
|
||||
eof: bool,
|
||||
error: Option<io::Error>,
|
||||
}
|
||||
impl Reader {
|
||||
pub fn new(file: Arc<dyn File>, offset: u64) -> Self {
|
||||
Self {
|
||||
file,
|
||||
offset,
|
||||
pending: None,
|
||||
data: Bytes::new(),
|
||||
eof: false,
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
fn start(&mut self) {
|
||||
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 }));
|
||||
}
|
||||
}
|
||||
}
|
||||
#[async_trait]
|
||||
impl Pollable for Reader {
|
||||
async fn ready(&mut self) {
|
||||
self.start();
|
||||
if let Some(task) = &mut self.pending {
|
||||
match task.await {
|
||||
Ok(data) => {
|
||||
self.eof = data.is_empty();
|
||||
self.offset += data.len() as u64;
|
||||
self.data = data.into();
|
||||
}
|
||||
Err(e) => self.error = Some(e),
|
||||
}
|
||||
self.pending = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
#[async_trait]
|
||||
impl InputStream for Reader {
|
||||
fn read(&mut self, size: usize) -> StreamResult<Bytes> {
|
||||
if let Some(e) = self.error.take() {
|
||||
return Err(failure(e));
|
||||
}
|
||||
if self.eof && self.data.is_empty() {
|
||||
return Err(StreamError::Closed);
|
||||
}
|
||||
let n = size.min(self.data.len());
|
||||
let data = self.data.split_to(n);
|
||||
self.start();
|
||||
Ok(data)
|
||||
}
|
||||
}
|
||||
pub(crate) struct Writer {
|
||||
file: Arc<dyn File>,
|
||||
offset: Option<u64>,
|
||||
pending: Option<Pending<usize>>,
|
||||
error: Option<io::Error>,
|
||||
}
|
||||
impl Writer {
|
||||
pub fn new(file: Arc<dyn File>, offset: Option<u64>) -> Self {
|
||||
Self {
|
||||
file,
|
||||
offset,
|
||||
pending: None,
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
#[async_trait]
|
||||
impl Pollable for Writer {
|
||||
async fn ready(&mut self) {
|
||||
if let Some(task) = &mut self.pending {
|
||||
match task.await {
|
||||
Ok(n) => {
|
||||
if let Some(p) = &mut self.offset {
|
||||
*p += n as u64
|
||||
}
|
||||
}
|
||||
Err(e) => self.error = Some(e),
|
||||
}
|
||||
self.pending = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
#[async_trait]
|
||||
impl OutputStream for Writer {
|
||||
fn write(&mut self, bytes: Bytes) -> StreamResult<()> {
|
||||
if self.check_write()? == 0 {
|
||||
return Err(StreamError::trap("write without a permit"));
|
||||
}
|
||||
let f = self.file.clone();
|
||||
let offset = self.offset;
|
||||
self.pending = Some(Box::pin(async move {
|
||||
let mut done = 0;
|
||||
while done < bytes.len() {
|
||||
let n = match offset {
|
||||
Some(p) => f.write(p + done as u64, bytes[done..].to_vec()).await?,
|
||||
None => f.append(bytes[done..].to_vec()).await?,
|
||||
};
|
||||
if n == 0 {
|
||||
return Err(error(io::ErrorKind::WriteZero));
|
||||
}
|
||||
done += n;
|
||||
}
|
||||
Ok(done)
|
||||
}));
|
||||
Ok(())
|
||||
}
|
||||
fn flush(&mut self) -> StreamResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn check_write(&mut self) -> StreamResult<usize> {
|
||||
if let Some(e) = self.error.take() {
|
||||
return Err(failure(e));
|
||||
}
|
||||
Ok(if self.pending.is_some() { 0 } else { 65536 })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user