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.
518 lines
17 KiB
Rust
518 lines
17 KiB
Rust
//! Wasmtime 49 filesystem interfaces backed exclusively by the virtual mount namespace.
|
|
//!
|
|
//! 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};
|
|
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);
|
|
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)]
|
|
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,
|
|
FileTooLarge => ErrorCode::FileTooLarge,
|
|
ResourceBusy => ErrorCode::Busy,
|
|
_ => 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.symlink {
|
|
types::DescriptorType::SymbolicLink
|
|
} else 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) -> FsResult<Option<SystemTime>> {
|
|
match t {
|
|
types::NewTimestamp::NoChange => Ok(None),
|
|
types::NewTimestamp::Now => Ok(Some(SystemTime::now())),
|
|
types::NewTimestamp::Timestamp(t) => {
|
|
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)?,
|
|
))
|
|
}
|
|
}
|
|
}
|
|
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,
|
|
symlink: false,
|
|
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.symlink {
|
|
types::DescriptorType::SymbolicLink
|
|
} else 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 follow = path_flags.contains(types::PathFlags::SYMLINK_FOLLOW);
|
|
let p = self.path(&fd, &path)?;
|
|
Ok(stat(
|
|
self.adapter.mounts.stat(&p, follow).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 follow = path_flags.contains(types::PathFlags::SYMLINK_FOLLOW);
|
|
let p = self.path(&fd, &path)?;
|
|
self.adapter
|
|
.mounts
|
|
.metadata_file(&p, follow)
|
|
.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 follow = path_flags.contains(types::PathFlags::SYMLINK_FOLLOW);
|
|
let path = self.path(&fd, &path)?;
|
|
let file = self
|
|
.adapter
|
|
.mounts
|
|
.open(
|
|
&path,
|
|
Open {
|
|
follow,
|
|
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 p = self.path(&fd, &path)?;
|
|
self.adapter.mounts.readlink(&p).await.map_err(errno)
|
|
}
|
|
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 follow = path_flags.contains(types::PathFlags::SYMLINK_FOLLOW);
|
|
let p = self.path(&fd, &path)?;
|
|
self.adapter.mounts.stat(&p, follow).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(())
|
|
}
|
|
}
|
|
|
|
#[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);
|
|
}
|
|
}
|