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(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user