//! 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, } 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( linker: &mut Linker, view: fn(&mut T) -> View<'_>, ) -> wasmtime::Result<()> { linker.allow_shadowing(true); let result = (|| { types::add_to_linker::(linker, view)?; preopens::add_to_linker::(linker, view) })(); linker.allow_shadowing(false); result?; Ok(()) } #[derive(Clone)] struct Descriptor { path: String, file: Arc, flags: DescriptorFlags, } struct Entries(std::vec::IntoIter); 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 { 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> { 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) -> FsResult { Ok(self .table .get(&Resource::::new_borrow(fd.rep()))? .clone()) } fn path(&self, fd: &Resource, path: &str) -> FsResult { Ok(resolve(&self.fd(fd)?.path, path)) } fn push(&mut self, descriptor: Descriptor) -> FsResult> { Ok(Resource::new_own(self.table.push(descriptor)?.rep())) } } impl preopens::Host for View<'_> { fn get_directories(&mut self) -> wasmtime::Result, String)>> { // The root descriptor needs no IO: directory operations use its virtual path. let file: Arc = 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 { Ok(Stat { directory: true, symlink: false, size: 0, accessed: None, modified: None, }) } async fn read(&self, _: u64, _: usize) -> Result> { Err(error(io::ErrorKind::IsADirectory)) } async fn write(&self, _: u64, _: Vec) -> Result { Err(error(io::ErrorKind::IsADirectory)) } async fn append(&self, _: Vec) -> Result { 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, _: Option) -> Result<()> { Err(error(io::ErrorKind::Unsupported)) } } impl types::Host for View<'_> { fn convert_error_code(&mut self, err: FsError) -> wasmtime::Result { err.downcast() } fn filesystem_error_code( &mut self, err: Resource, ) -> wasmtime::Result> { Ok(self.table.get(&err)?.downcast_ref::().map(code)) } } impl HostDescriptor for View<'_> { async fn advise( &mut self, fd: Resource, offset: types::Filesize, len: types::Filesize, advice: types::Advice, ) -> FsResult<()> { let _ = (fd, offset, len, advice); Ok(()) } async fn sync_data(&mut self, fd: Resource) -> FsResult<()> { self.fd(&fd)?.file.sync().await.map_err(errno) } async fn get_flags(&mut self, fd: Resource) -> FsResult { Ok(self.fd(&fd)?.flags) } async fn get_type(&mut self, fd: Resource) -> FsResult { Ok(stat(self.fd(&fd)?.file.stat().await.map_err(errno)?).type_) } async fn set_size( &mut self, fd: Resource, size: types::Filesize, ) -> FsResult<()> { self.fd(&fd)?.file.set_size(size).await.map_err(errno) } async fn set_times( &mut self, fd: Resource, 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, len: types::Filesize, offset: types::Filesize, ) -> FsResult<(Vec, 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, buf: Vec, offset: types::Filesize, ) -> FsResult { self.fd(&fd)? .file .write(offset, buf) .await .map(|n| n as u64) .map_err(errno) } async fn read_directory( &mut self, fd: Resource, ) -> FsResult> { 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::>(); Ok(Resource::new_own( self.table.push(Entries(entries.into_iter()))?.rep(), )) } async fn sync(&mut self, fd: Resource) -> FsResult<()> { self.fd(&fd)?.file.sync().await.map_err(errno) } async fn create_directory_at( &mut self, fd: Resource, 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) -> FsResult { Ok(stat(self.fd(&fd)?.file.stat().await.map_err(errno)?)) } async fn stat_at( &mut self, fd: Resource, path_flags: types::PathFlags, path: String, ) -> FsResult { 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, 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, old_path_flags: types::PathFlags, old_path: String, new_descriptor: Resource, 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, path_flags: types::PathFlags, path: String, oflags: OpenFlags, flags: DescriptorFlags, ) -> FsResult> { 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) -> wasmtime::Result<()> { self.table .delete(Resource::::new_own(fd.rep()))?; Ok(()) } async fn readlink_at( &mut self, fd: Resource, path: String, ) -> FsResult { let p = self.path(&fd, &path)?; self.adapter.mounts.readlink(&p).await.map_err(errno) } async fn remove_directory_at( &mut self, fd: Resource, 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, old_path: String, new_fd: Resource, 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, 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, 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, offset: types::Filesize, ) -> FsResult> { 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, offset: types::Filesize, ) -> FsResult> { 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, ) -> FsResult> { 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, b: Resource, ) -> wasmtime::Result { Ok(self.fd(&a)?.path == self.fd(&b)?.path) } async fn metadata_hash( &mut self, fd: Resource, ) -> FsResult { Ok(hash(&self.fd(&fd)?.path)) } async fn metadata_hash_at( &mut self, fd: Resource, path_flags: types::PathFlags, path: String, ) -> FsResult { 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, ) -> FsResult> { Ok(self .table .get_mut(&Resource::::new_borrow(stream.rep()))? .0 .next()) } fn drop(&mut self, stream: Resource) -> wasmtime::Result<()> { self.table .delete(Resource::::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); } }