diff --git a/crates/test-programs/build.rs b/crates/test-programs/build.rs index c7c9460623..44b62b5def 100644 --- a/crates/test-programs/build.rs +++ b/crates/test-programs/build.rs @@ -11,11 +11,19 @@ fn main() { #[cfg(feature = "test_programs")] mod wasi_tests { use std::env; - use std::fs::{read_dir, DirEntry, File}; + use std::fs::{read_dir, File}; use std::io::{self, Write}; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; + #[derive(Clone, Copy, Debug)] + enum PreopenType { + /// Preopens should be satisfied with real OS files. + OS, + /// Preopens should be satisfied with virtual files. + Virtual, + } + pub(super) fn build_and_generate_tests() { // Validate if any of test sources are present and if they changed // This should always work since there is no submodule to init anymore @@ -103,8 +111,21 @@ mod wasi_tests { .replace("-", "_") )?; writeln!(out, " use super::{{runtime, utils, setup_log}};")?; + writeln!(out, " use runtime::PreopenType;")?; for dir_entry in dir_entries { - write_testsuite_tests(out, dir_entry, testsuite)?; + let test_path = dir_entry.path(); + let stemstr = test_path + .file_stem() + .expect("file_stem") + .to_str() + .expect("to_str"); + + if no_preopens(testsuite, stemstr) { + write_testsuite_tests(out, &test_path, testsuite, PreopenType::OS)?; + } else { + write_testsuite_tests(out, &test_path, testsuite, PreopenType::OS)?; + write_testsuite_tests(out, &test_path, testsuite, PreopenType::Virtual)?; + } } writeln!(out, "}}")?; Ok(()) @@ -112,10 +133,10 @@ mod wasi_tests { fn write_testsuite_tests( out: &mut File, - dir_entry: DirEntry, + path: &Path, testsuite: &str, + preopen_type: PreopenType, ) -> io::Result<()> { - let path = dir_entry.path(); let stemstr = path .file_stem() .expect("file_stem") @@ -123,14 +144,19 @@ mod wasi_tests { .expect("to_str"); writeln!(out, " #[test]")?; - if ignore(testsuite, stemstr) { + let test_fn_name = format!( + "{}{}", + &stemstr.replace("-", "_"), + if let PreopenType::Virtual = preopen_type { + "_virtualfs" + } else { + "" + } + ); + if ignore(testsuite, &test_fn_name) { writeln!(out, " #[ignore]")?; } - writeln!( - out, - " fn r#{}() -> anyhow::Result<()> {{", - &stemstr.replace("-", "_") - )?; + writeln!(out, " fn r#{}() -> anyhow::Result<()> {{", test_fn_name,)?; writeln!(out, " setup_log();")?; writeln!( out, @@ -145,16 +171,25 @@ mod wasi_tests { let workspace = if no_preopens(testsuite, stemstr) { "None" } else { - writeln!( - out, - " let workspace = utils::prepare_workspace(&bin_name)?;" - )?; - "Some(workspace.path())" + match preopen_type { + PreopenType::OS => { + writeln!( + out, + " let workspace = utils::prepare_workspace(&bin_name)?;" + )?; + "Some(workspace.path())" + } + PreopenType::Virtual => "Some(std::path::Path::new(&bin_name))", + } }; writeln!( out, - " runtime::instantiate(&data, &bin_name, {})", - workspace + " runtime::instantiate(&data, &bin_name, {}, {})", + workspace, + match preopen_type { + PreopenType::OS => "PreopenType::OS", + PreopenType::Virtual => "PreopenType::Virtual", + } )?; writeln!(out, " }}")?; writeln!(out)?; @@ -164,8 +199,30 @@ mod wasi_tests { cfg_if::cfg_if! { if #[cfg(not(windows))] { /// Ignore tests that aren't supported yet. - fn ignore(_testsuite: &str, _name: &str) -> bool { - false + fn ignore(testsuite: &str, name: &str) -> bool { + if testsuite == "wasi-tests" { + match name { + // TODO: virtfs files cannot be poll_oneoff'd yet + "poll_oneoff_virtualfs" => true, + // TODO: virtfs does not support filetimes yet. + "path_filestat_virtualfs" | + "fd_filestat_set_virtualfs" => true, + // TODO: virtfs does not support symlinks yet. + "nofollow_errors_virtualfs" | + "path_link_virtualfs" | + "readlink_virtualfs" | + "readlink_no_buffer_virtualfs" | + "dangling_symlink_virtualfs" | + "symlink_loop_virtualfs" | + "path_symlink_trailing_slashes_virtualfs" => true, + // TODO: virtfs does not support rename yet. + "path_rename_trailing_slashes_virtualfs" | + "path_rename_virtualfs" => true, + _ => false, + } + } else { + unreachable!() + } } } else { /// Ignore tests that aren't supported yet. @@ -178,6 +235,22 @@ mod wasi_tests { "truncation_rights" => true, "path_link" => true, "dangling_fd" => true, + // TODO: virtfs files cannot be poll_oneoff'd yet + "poll_oneoff_virtualfs" => true, + // TODO: virtfs does not support filetimes yet. + "path_filestat_virtualfs" | + "fd_filestat_set_virtualfs" => true, + // TODO: virtfs does not support symlinks yet. + "nofollow_errors_virtualfs" | + "path_link_virtualfs" | + "readlink_virtualfs" | + "readlink_no_buffer_virtualfs" | + "dangling_symlink_virtualfs" | + "symlink_loop_virtualfs" | + "path_symlink_trailing_slashes_virtualfs" => true, + // TODO: virtfs does not support rename yet. + "path_rename_trailing_slashes_virtualfs" | + "path_rename_virtualfs" => true, _ => false, } } else { diff --git a/crates/test-programs/tests/wasm_tests/runtime.rs b/crates/test-programs/tests/wasm_tests/runtime.rs index 42d4c7221b..019990bd96 100644 --- a/crates/test-programs/tests/wasm_tests/runtime.rs +++ b/crates/test-programs/tests/wasm_tests/runtime.rs @@ -1,30 +1,44 @@ use anyhow::{bail, Context}; use std::fs::File; use std::path::Path; +use wasi_common::VirtualDirEntry; use wasmtime::{Instance, Module, Store}; -pub fn instantiate(data: &[u8], bin_name: &str, workspace: Option<&Path>) -> anyhow::Result<()> { +#[derive(Clone, Copy, Debug)] +pub enum PreopenType { + /// Preopens should be satisfied with real OS files. + OS, + /// Preopens should be satisfied with virtual files. + Virtual, +} + +pub fn instantiate( + data: &[u8], + bin_name: &str, + workspace: Option<&Path>, + preopen_type: PreopenType, +) -> anyhow::Result<()> { let store = Store::default(); - let get_preopens = |workspace: Option<&Path>| -> anyhow::Result> { - if let Some(workspace) = workspace { - let preopen_dir = wasi_common::preopen_dir(workspace) - .context(format!("error while preopening {:?}", workspace))?; - - Ok(vec![(".".to_owned(), preopen_dir)]) - } else { - Ok(vec![]) - } - }; - // Create our wasi context with pretty standard arguments/inheritance/etc. - // Additionally register andy preopened directories if we have them. + // Additionally register any preopened directories if we have them. let mut builder = wasi_common::WasiCtxBuilder::new(); builder.arg(bin_name).arg(".").inherit_stdio(); - for (dir, file) in get_preopens(workspace)? { - builder.preopened_dir(file, dir); + if let Some(workspace) = workspace { + match preopen_type { + PreopenType::OS => { + let preopen_dir = wasi_common::preopen_dir(workspace) + .context(format!("error while preopening {:?}", workspace))?; + builder.preopened_dir(preopen_dir, "."); + } + PreopenType::Virtual => { + // we can ignore the workspace path for virtual preopens because virtual preopens + // don't exist in the filesystem anyway - no name conflict concerns. + builder.preopened_virt(VirtualDirEntry::empty_directory(), "."); + } + } } // The nonstandard thing we do with `WasiCtxBuilder` is to ensure that diff --git a/crates/test-programs/wasi-tests/src/bin/fd_flags_set.rs b/crates/test-programs/wasi-tests/src/bin/fd_flags_set.rs index e642ca19a5..c58ff9247e 100644 --- a/crates/test-programs/wasi-tests/src/bin/fd_flags_set.rs +++ b/crates/test-programs/wasi-tests/src/bin/fd_flags_set.rs @@ -50,7 +50,7 @@ unsafe fn test_fd_fdstat_set_flags(dir_fd: wasi::Fd) { ) .expect("reading file"), buffer.len(), - "shoudl read {} bytes", + "should read {} bytes", buffer.len() ); @@ -87,7 +87,7 @@ unsafe fn test_fd_fdstat_set_flags(dir_fd: wasi::Fd) { ) .expect("reading file"), buffer.len(), - "shoudl read {} bytes", + "should read {} bytes", buffer.len() ); @@ -126,7 +126,7 @@ unsafe fn test_fd_fdstat_set_flags(dir_fd: wasi::Fd) { ) .expect("reading file"), buffer.len(), - "shoudl read {} bytes", + "should read {} bytes", buffer.len() ); diff --git a/crates/test-programs/wasi-tests/src/bin/remove_directory_trailing_slashes.rs b/crates/test-programs/wasi-tests/src/bin/remove_directory_trailing_slashes.rs index 7f05199dc3..a77f7878c2 100644 --- a/crates/test-programs/wasi-tests/src/bin/remove_directory_trailing_slashes.rs +++ b/crates/test-programs/wasi-tests/src/bin/remove_directory_trailing_slashes.rs @@ -11,14 +11,14 @@ unsafe fn test_remove_directory_trailing_slashes(dir_fd: wasi::Fd) { wasi::path_create_directory(dir_fd, "dir").expect("creating a directory"); - // Test that removing it with a trailing flash succeeds. + // Test that removing it with a trailing slash succeeds. wasi::path_remove_directory(dir_fd, "dir/") .expect("remove_directory with a trailing slash on a directory should succeed"); // Create a temporary file. create_file(dir_fd, "file"); - // Test that removing it with no trailing flash fails. + // Test that removing it with no trailing slash fails. assert_eq!( wasi::path_remove_directory(dir_fd, "file") .expect_err("remove_directory without a trailing slash on a file should fail") @@ -27,7 +27,7 @@ unsafe fn test_remove_directory_trailing_slashes(dir_fd: wasi::Fd) { "errno should be ERRNO_NOTDIR" ); - // Test that removing it with a trailing flash fails. + // Test that removing it with a trailing slash fails. assert_eq!( wasi::path_remove_directory(dir_fd, "file/") .expect_err("remove_directory with a trailing slash on a file should fail") diff --git a/crates/wasi-common/src/ctx.rs b/crates/wasi-common/src/ctx.rs index e9e64a3866..981007c515 100644 --- a/crates/wasi-common/src/ctx.rs +++ b/crates/wasi-common/src/ctx.rs @@ -1,4 +1,6 @@ -use crate::fdentry::FdEntry; +use crate::fdentry::{Descriptor, FdEntry}; +use crate::sys::fdentry_impl::OsHandle; +use crate::virtfs::{VirtualDir, VirtualDirEntry}; use crate::{wasi, Error, Result}; use std::borrow::Borrow; use std::collections::HashMap; @@ -61,7 +63,7 @@ impl PendingCString { /// A builder allowing customizable construction of `WasiCtx` instances. pub struct WasiCtxBuilder { fds: Option>, - preopens: Option>, + preopens: Option>, args: Option>, env: Option>, } @@ -222,10 +224,46 @@ impl WasiCtxBuilder { /// Add a preopened directory. pub fn preopened_dir>(&mut self, dir: File, guest_path: P) -> &mut Self { + self.preopens.as_mut().unwrap().push(( + guest_path.as_ref().to_owned(), + Descriptor::OsHandle(OsHandle::from(dir)), + )); + self + } + + /// Add a preopened virtual directory. + pub fn preopened_virt>( + &mut self, + dir: VirtualDirEntry, + guest_path: P, + ) -> &mut Self { + fn populate_directory(virtentry: HashMap, dir: &mut VirtualDir) { + for (path, entry) in virtentry.into_iter() { + match entry { + VirtualDirEntry::Directory(dir_entries) => { + let mut subdir = VirtualDir::new(true); + populate_directory(dir_entries, &mut subdir); + dir.add_dir(subdir, path); + } + VirtualDirEntry::File(content) => { + dir.add_file(content, path); + } + } + } + } + + let dir = if let VirtualDirEntry::Directory(entries) = dir { + let mut dir = VirtualDir::new(true); + populate_directory(entries, &mut dir); + Box::new(dir) + } else { + panic!("the root of a VirtualDirEntry tree must be a VirtualDirEntry::Directory"); + }; + self.preopens .as_mut() .unwrap() - .push((guest_path.as_ref().to_owned(), dir)); + .push((guest_path.as_ref().to_owned(), Descriptor::VirtualFile(dir))); self } @@ -271,7 +309,7 @@ impl WasiCtxBuilder { fds.insert(fd, f()?); } PendingFdEntry::File(f) => { - fds.insert(fd, FdEntry::from(f)?); + fds.insert(fd, FdEntry::from(Descriptor::OsHandle(OsHandle::from(f)))?); } } } @@ -284,8 +322,20 @@ impl WasiCtxBuilder { // unnecessarily if we have exactly the maximum number of file descriptors. preopen_fd = preopen_fd.checked_add(1).ok_or(Error::ENFILE)?; - if !dir.metadata()?.is_dir() { - return Err(Error::EBADF); + match &dir { + Descriptor::OsHandle(handle) => { + if !handle.metadata()?.is_dir() { + return Err(Error::EBADF); + } + } + Descriptor::VirtualFile(virt) => { + if virt.get_file_type() != wasi::__WASI_FILETYPE_DIRECTORY { + return Err(Error::EBADF); + } + } + Descriptor::Stdin | Descriptor::Stdout | Descriptor::Stderr => { + panic!("implementation error, stdin/stdout/stderr shouldn't be in the list of preopens"); + } } // We don't currently allow setting file descriptors other than 0-2, but this will avoid diff --git a/crates/wasi-common/src/fdentry.rs b/crates/wasi-common/src/fdentry.rs index a86e6ce8b4..c2c9a719da 100644 --- a/crates/wasi-common/src/fdentry.rs +++ b/crates/wasi-common/src/fdentry.rs @@ -2,36 +2,75 @@ use crate::sys::dev_null; use crate::sys::fdentry_impl::{ descriptor_as_oshandle, determine_type_and_access_rights, OsHandle, }; +use crate::virtfs::VirtualFile; use crate::{wasi, Error, Result}; use std::marker::PhantomData; use std::mem::ManuallyDrop; use std::ops::{Deref, DerefMut}; use std::path::PathBuf; -use std::{fs, io}; +use std::{fmt, fs, io}; -#[derive(Debug)] pub(crate) enum Descriptor { OsHandle(OsHandle), + VirtualFile(Box), Stdin, Stdout, Stderr, } -impl Descriptor { - /// Return a reference to the `OsHandle` treating it as an actual file/dir, and - /// allowing operations which require an actual file and not just a stream or - /// socket file descriptor. - pub(crate) fn as_file(&self) -> Result<&OsHandle> { +impl From for Descriptor { + fn from(handle: OsHandle) -> Self { + Descriptor::OsHandle(handle) + } +} + +impl From> for Descriptor { + fn from(virt: Box) -> Self { + Descriptor::VirtualFile(virt) + } +} + +impl fmt::Debug for Descriptor { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - Self::OsHandle(file) => Ok(file), + Descriptor::OsHandle(handle) => write!(f, "{:?}", handle), + Descriptor::VirtualFile(_) => write!(f, "VirtualFile"), + Descriptor::Stdin => write!(f, "Stdin"), + Descriptor::Stdout => write!(f, "Stdout"), + Descriptor::Stderr => write!(f, "Stderr"), + } + } +} + +impl Descriptor { + pub(crate) fn try_clone(&self) -> io::Result { + match self { + Descriptor::OsHandle(file) => file.try_clone().map(|f| OsHandle::from(f).into()), + Descriptor::VirtualFile(virt) => virt.try_clone().map(Descriptor::VirtualFile), + Descriptor::Stdin => Ok(Descriptor::Stdin), + Descriptor::Stdout => Ok(Descriptor::Stdout), + Descriptor::Stderr => Ok(Descriptor::Stderr), + } + } + + /// Return a reference to the `OsHandle` or `VirtualFile` treating it as an + /// actual file/dir, and allowing operations which require an actual file and + /// not just a stream or socket file descriptor. + pub(crate) fn as_file<'descriptor>(&'descriptor self) -> Result<&'descriptor Descriptor> { + match self { + Self::OsHandle(_) => Ok(self), + Self::VirtualFile(_) => Ok(self), _ => Err(Error::EBADF), } } /// Like `as_file`, but return a mutable reference. - pub(crate) fn as_file_mut(&mut self) -> Result<&mut OsHandle> { + pub(crate) fn as_file_mut<'descriptor>( + &'descriptor mut self, + ) -> Result<&'descriptor mut Descriptor> { match self { - Self::OsHandle(file) => Ok(file), + Self::OsHandle(_) => Ok(self), + Self::VirtualFile(_) => Ok(self), _ => Err(Error::EBADF), } } @@ -61,16 +100,33 @@ pub(crate) struct FdEntry { } impl FdEntry { - pub(crate) fn from(file: fs::File) -> Result { - unsafe { determine_type_and_access_rights(&file) }.map( - |(file_type, rights_base, rights_inheriting)| Self { - file_type, - descriptor: Descriptor::OsHandle(OsHandle::from(file)), - rights_base, - rights_inheriting, - preopen_path: None, - }, - ) + pub(crate) fn from(file: Descriptor) -> Result { + match file { + Descriptor::OsHandle(handle) => unsafe { determine_type_and_access_rights(&handle) } + .map(|(file_type, rights_base, rights_inheriting)| Self { + file_type, + descriptor: handle.into(), + rights_base, + rights_inheriting, + preopen_path: None, + }), + Descriptor::VirtualFile(virt) => { + let file_type = virt.get_file_type(); + let rights_base = virt.get_rights_base(); + let rights_inheriting = virt.get_rights_inheriting(); + + Ok(Self { + file_type, + descriptor: virt.into(), + rights_base, + rights_inheriting, + preopen_path: None, + }) + } + Descriptor::Stdin | Descriptor::Stdout | Descriptor::Stderr => { + panic!("implementation error, stdin/stdout/stderr FdEntry must not be constructed from FdEntry::from"); + } + } } pub(crate) fn duplicate_stdin() -> Result { @@ -110,7 +166,7 @@ impl FdEntry { } pub(crate) fn null() -> Result { - Self::from(dev_null()?) + Self::from(OsHandle::from(dev_null()?).into()) } /// Convert this `FdEntry` into a host `Descriptor` object provided the specified diff --git a/crates/wasi-common/src/host.rs b/crates/wasi-common/src/host.rs index 946d7feb12..47221e2763 100644 --- a/crates/wasi-common/src/host.rs +++ b/crates/wasi-common/src/host.rs @@ -22,9 +22,9 @@ pub(crate) unsafe fn iovec_to_host_mut(iovec: &mut __wasi_iovec_t) -> io::IoSlic } #[allow(dead_code)] // trouble with sockets -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq)] #[repr(u8)] -pub(crate) enum FileType { +pub enum FileType { Unknown = __WASI_FILETYPE_UNKNOWN, BlockDevice = __WASI_FILETYPE_BLOCK_DEVICE, CharacterDevice = __WASI_FILETYPE_CHARACTER_DEVICE, @@ -39,10 +39,25 @@ impl FileType { pub(crate) fn to_wasi(&self) -> __wasi_filetype_t { *self as __wasi_filetype_t } + + pub(crate) fn from_wasi(wasi_filetype: u8) -> Option { + use FileType::*; + match wasi_filetype { + __WASI_FILETYPE_UNKNOWN => Some(Unknown), + __WASI_FILETYPE_BLOCK_DEVICE => Some(BlockDevice), + __WASI_FILETYPE_CHARACTER_DEVICE => Some(CharacterDevice), + __WASI_FILETYPE_DIRECTORY => Some(Directory), + __WASI_FILETYPE_REGULAR_FILE => Some(RegularFile), + __WASI_FILETYPE_SOCKET_DGRAM => Some(SocketDgram), + __WASI_FILETYPE_SOCKET_STREAM => Some(SocketStream), + __WASI_FILETYPE_SYMBOLIC_LINK => Some(Symlink), + _ => None, + } + } } #[derive(Debug, Clone)] -pub(crate) struct Dirent { +pub struct Dirent { pub name: String, pub ftype: FileType, pub ino: u64, diff --git a/crates/wasi-common/src/hostcalls_impl/fs.rs b/crates/wasi-common/src/hostcalls_impl/fs.rs index d5ecb8bc69..e10009242e 100644 --- a/crates/wasi-common/src/hostcalls_impl/fs.rs +++ b/crates/wasi-common/src/hostcalls_impl/fs.rs @@ -3,6 +3,7 @@ use super::fs_helpers::path_get; use crate::ctx::WasiCtx; use crate::fdentry::{Descriptor, FdEntry}; use crate::helpers::*; +use crate::host::Dirent; use crate::memory::*; use crate::sandboxed_tty_writer::SandboxedTTYWriter; use crate::sys::hostcalls_impl::fs_helpers::path_open_rights; @@ -10,7 +11,7 @@ use crate::sys::{host_impl, hostcalls_impl}; use crate::{helpers, host, wasi, wasi32, Error, Result}; use filetime::{set_file_handle_times, FileTime}; use log::trace; -use std::fs::File; +use std::convert::TryInto; use std::io::{self, Read, Seek, SeekFrom, Write}; use std::ops::DerefMut; use std::time::{Duration, SystemTime, UNIX_EPOCH}; @@ -40,12 +41,15 @@ pub(crate) unsafe fn fd_datasync( ) -> Result<()> { trace!("fd_datasync(fd={:?})", fd); - let fd = wasi_ctx + let file = wasi_ctx .get_fd_entry(fd)? - .as_descriptor(wasi::__WASI_RIGHTS_FD_DATASYNC, 0)? - .as_file()?; + .as_descriptor(wasi::__WASI_RIGHTS_FD_DATASYNC, 0)?; - fd.sync_data().map_err(Into::into) + match file { + Descriptor::OsHandle(fd) => fd.sync_data().map_err(Into::into), + Descriptor::VirtualFile(virt) => virt.datasync(), + other => other.as_os_handle().sync_data().map_err(Into::into), + } } pub(crate) unsafe fn fd_pread( @@ -66,7 +70,7 @@ pub(crate) unsafe fn fd_pread( nread ); - let fd = wasi_ctx + let file = wasi_ctx .get_fd_entry(fd)? .as_descriptor(wasi::__WASI_RIGHTS_FD_READ | wasi::__WASI_RIGHTS_FD_SEEK, 0)? .as_file()?; @@ -76,9 +80,28 @@ pub(crate) unsafe fn fd_pread( if offset > i64::max_value() as u64 { return Err(Error::EIO); } - let buf_size = iovs.iter().map(|v| v.buf_len).sum(); - let mut buf = vec![0; buf_size]; - let host_nread = hostcalls_impl::fd_pread(fd, &mut buf, offset)?; + let buf_size = iovs + .iter() + .map(|iov| { + let cast_iovlen: wasi32::size_t = iov + .buf_len + .try_into() + .expect("iovec are bounded by wasi max sizes"); + cast_iovlen + }) + .fold(Some(0u32), |len, iov| len.and_then(|x| x.checked_add(iov))) + .ok_or(Error::EINVAL)?; + let mut buf = vec![0; buf_size as usize]; + let host_nread = match file { + Descriptor::OsHandle(fd) => hostcalls_impl::fd_pread(&fd, &mut buf, offset)?, + Descriptor::VirtualFile(virt) => virt.pread(&mut buf, offset)?, + _ => { + unreachable!( + "implementation error: fd should have been checked to not be a stream already" + ); + } + }; + let mut buf_offset = 0; let mut left = host_nread; for iov in &iovs { @@ -115,7 +138,7 @@ pub(crate) unsafe fn fd_pwrite( nwritten ); - let fd = wasi_ctx + let file = wasi_ctx .get_fd_entry(fd)? .as_descriptor( wasi::__WASI_RIGHTS_FD_WRITE | wasi::__WASI_RIGHTS_FD_SEEK, @@ -127,15 +150,33 @@ pub(crate) unsafe fn fd_pwrite( if offset > i64::max_value() as u64 { return Err(Error::EIO); } - let buf_size = iovs.iter().map(|v| v.buf_len).sum(); - let mut buf = Vec::with_capacity(buf_size); + let buf_size = iovs + .iter() + .map(|iov| { + let cast_iovlen: wasi32::size_t = iov + .buf_len + .try_into() + .expect("iovec are bounded by wasi max sizes"); + cast_iovlen + }) + .fold(Some(0u32), |len, iov| len.and_then(|x| x.checked_add(iov))) + .ok_or(Error::EINVAL)?; + let mut buf = Vec::with_capacity(buf_size as usize); for iov in &iovs { buf.extend_from_slice(std::slice::from_raw_parts( iov.buf as *const u8, iov.buf_len, )); } - let host_nwritten = hostcalls_impl::fd_pwrite(fd, &buf, offset)?; + let host_nwritten = match file { + Descriptor::OsHandle(fd) => hostcalls_impl::fd_pwrite(&fd, &buf, offset)?, + Descriptor::VirtualFile(virt) => virt.pwrite(buf.as_mut(), offset)?, + _ => { + unreachable!( + "implementation error: fd should have been checked to not be a stream already" + ); + } + }; trace!(" | *nwritten={:?}", host_nwritten); @@ -168,8 +209,9 @@ pub(crate) unsafe fn fd_read( .get_fd_entry_mut(fd)? .as_descriptor_mut(wasi::__WASI_RIGHTS_FD_READ, 0)? { - Descriptor::OsHandle(file) => file.read_vectored(&mut iovs), - Descriptor::Stdin => io::stdin().read_vectored(&mut iovs), + Descriptor::OsHandle(file) => file.read_vectored(&mut iovs).map_err(Into::into), + Descriptor::VirtualFile(virt) => virt.read_vectored(&mut iovs), + Descriptor::Stdin => io::stdin().read_vectored(&mut iovs).map_err(Into::into), _ => return Err(Error::EBADF), }; @@ -232,7 +274,7 @@ pub(crate) unsafe fn fd_seek( } else { wasi::__WASI_RIGHTS_FD_SEEK | wasi::__WASI_RIGHTS_FD_TELL }; - let fd = wasi_ctx + let file = wasi_ctx .get_fd_entry_mut(fd)? .as_descriptor_mut(rights, 0)? .as_file_mut()?; @@ -243,7 +285,15 @@ pub(crate) unsafe fn fd_seek( wasi::__WASI_WHENCE_SET => SeekFrom::Start(offset as u64), _ => return Err(Error::EINVAL), }; - let host_newoffset = fd.seek(pos)?; + let host_newoffset = match file { + Descriptor::OsHandle(fd) => fd.seek(pos)?, + Descriptor::VirtualFile(virt) => virt.seek(pos)?, + _ => { + unreachable!( + "implementation error: fd should have been checked to not be a stream already" + ); + } + }; trace!(" | *newoffset={:?}", host_newoffset); @@ -258,12 +308,20 @@ pub(crate) unsafe fn fd_tell( ) -> Result<()> { trace!("fd_tell(fd={:?}, newoffset={:#x?})", fd, newoffset); - let fd = wasi_ctx + let file = wasi_ctx .get_fd_entry_mut(fd)? .as_descriptor_mut(wasi::__WASI_RIGHTS_FD_TELL, 0)? .as_file_mut()?; - let host_offset = fd.seek(SeekFrom::Current(0))?; + let host_offset = match file { + Descriptor::OsHandle(fd) => fd.seek(SeekFrom::Current(0))?, + Descriptor::VirtualFile(virt) => virt.seek(SeekFrom::Current(0))?, + _ => { + unreachable!( + "implementation error: fd should have been checked to not be a stream already" + ); + } + }; trace!(" | *newoffset={:?}", host_offset); @@ -279,12 +337,13 @@ pub(crate) unsafe fn fd_fdstat_get( trace!("fd_fdstat_get(fd={:?}, fdstat_ptr={:#x?})", fd, fdstat_ptr); let mut fdstat = dec_fdstat_byref(memory, fdstat_ptr)?; - let host_fd = wasi_ctx - .get_fd_entry(fd)? - .as_descriptor(0, 0)? - .as_os_handle(); + let wasi_file = wasi_ctx.get_fd_entry(fd)?.as_descriptor(0, 0)?; - let fs_flags = hostcalls_impl::fd_fdstat_get(&host_fd)?; + let fs_flags = match wasi_file { + Descriptor::OsHandle(wasi_fd) => hostcalls_impl::fd_fdstat_get(&wasi_fd)?, + Descriptor::VirtualFile(virt) => virt.fdstat_get(), + other => hostcalls_impl::fd_fdstat_get(&other.as_os_handle())?, + }; let fe = wasi_ctx.get_fd_entry(fd)?; fdstat.fs_filetype = fe.file_type; @@ -309,11 +368,28 @@ pub(crate) unsafe fn fd_fdstat_set_flags( .get_fd_entry_mut(fd)? .as_descriptor_mut(wasi::__WASI_RIGHTS_FD_FDSTAT_SET_FLAGS, 0)?; - if let Some(new_handle) = - hostcalls_impl::fd_fdstat_set_flags(&descriptor.as_os_handle(), fdflags)? - { - *descriptor = Descriptor::OsHandle(new_handle); - } + match descriptor { + Descriptor::OsHandle(handle) => { + let set_result = + hostcalls_impl::fd_fdstat_set_flags(&handle, fdflags)?.map(Descriptor::OsHandle); + + if let Some(new_descriptor) = set_result { + *descriptor = new_descriptor; + } + } + Descriptor::VirtualFile(handle) => { + handle.fdstat_set_flags(fdflags)?; + } + _ => { + let set_result = + hostcalls_impl::fd_fdstat_set_flags(&descriptor.as_os_handle(), fdflags)? + .map(Descriptor::OsHandle); + + if let Some(new_descriptor) = set_result { + *descriptor = new_descriptor; + } + } + }; Ok(()) } @@ -351,11 +427,19 @@ pub(crate) unsafe fn fd_sync( ) -> Result<()> { trace!("fd_sync(fd={:?})", fd); - let fd = wasi_ctx + let file = wasi_ctx .get_fd_entry(fd)? .as_descriptor(wasi::__WASI_RIGHTS_FD_SYNC, 0)? .as_file()?; - fd.sync_all().map_err(Into::into) + match file { + Descriptor::OsHandle(fd) => fd.sync_all().map_err(Into::into), + Descriptor::VirtualFile(virt) => virt.sync(), + _ => { + unreachable!( + "implementation error: fd should have been checked to not be a stream already" + ); + } + } } pub(crate) unsafe fn fd_write( @@ -389,6 +473,13 @@ pub(crate) unsafe fn fd_write( file.write_vectored(&iovs)? } } + Descriptor::VirtualFile(virt) => { + if isatty { + unimplemented!("writes to virtual tty"); + } else { + virt.write_vectored(&iovs)? + } + } Descriptor::Stdin => return Err(Error::EBADF), Descriptor::Stdout => { // lock for the duration of the scope @@ -415,7 +506,7 @@ pub(crate) unsafe fn fd_write( } pub(crate) unsafe fn fd_advise( - wasi_ctx: &WasiCtx, + wasi_ctx: &mut WasiCtx, _memory: &mut [u8], fd: wasi::__wasi_fd_t, offset: wasi::__wasi_filesize_t, @@ -430,12 +521,20 @@ pub(crate) unsafe fn fd_advise( advice ); - let fd = wasi_ctx - .get_fd_entry(fd)? - .as_descriptor(wasi::__WASI_RIGHTS_FD_ADVISE, 0)? - .as_file()?; + let file = wasi_ctx + .get_fd_entry_mut(fd)? + .as_descriptor_mut(wasi::__WASI_RIGHTS_FD_ADVISE, 0)? + .as_file_mut()?; - hostcalls_impl::fd_advise(fd, advice, offset, len) + match file { + Descriptor::OsHandle(fd) => hostcalls_impl::fd_advise(&fd, advice, offset, len), + Descriptor::VirtualFile(virt) => virt.advise(advice, offset, len), + _ => { + unreachable!( + "implementation error: fd should have been checked to not be a stream already" + ); + } + } } pub(crate) unsafe fn fd_allocate( @@ -447,24 +546,34 @@ pub(crate) unsafe fn fd_allocate( ) -> Result<()> { trace!("fd_allocate(fd={:?}, offset={}, len={})", fd, offset, len); - let fd = wasi_ctx + let file = wasi_ctx .get_fd_entry(fd)? .as_descriptor(wasi::__WASI_RIGHTS_FD_ALLOCATE, 0)? .as_file()?; - let metadata = fd.metadata()?; + match file { + Descriptor::OsHandle(fd) => { + let metadata = fd.metadata()?; - let current_size = metadata.len(); - let wanted_size = offset.checked_add(len).ok_or(Error::E2BIG)?; - // This check will be unnecessary when rust-lang/rust#63326 is fixed - if wanted_size > i64::max_value() as u64 { - return Err(Error::E2BIG); - } + let current_size = metadata.len(); + let wanted_size = offset.checked_add(len).ok_or(Error::E2BIG)?; + // This check will be unnecessary when rust-lang/rust#63326 is fixed + if wanted_size > i64::max_value() as u64 { + return Err(Error::E2BIG); + } - if wanted_size > current_size { - fd.set_len(wanted_size).map_err(Into::into) - } else { - Ok(()) + if wanted_size > current_size { + fd.set_len(wanted_size).map_err(Into::into) + } else { + Ok(()) + } + } + Descriptor::VirtualFile(virt) => virt.allocate(offset, len), + _ => { + unreachable!( + "implementation error: fd should have been checked to not be a stream already" + ); + } } } @@ -490,7 +599,7 @@ pub(crate) unsafe fn path_create_directory( let fe = wasi_ctx.get_fd_entry(dirfd)?; let resolved = path_get(fe, rights, 0, 0, path, false)?; - hostcalls_impl::path_create_directory(resolved) + resolved.path_create_directory() } pub(crate) unsafe fn path_link( @@ -607,7 +716,7 @@ pub(crate) unsafe fn path_open( read, write ); - let fd = hostcalls_impl::path_open(resolved, read, write, oflags, fs_flags)?; + let fd = resolved.open_with(read, write, oflags, fs_flags)?; let mut fe = FdEntry::from(fd)?; // We need to manually deny the rights which are not explicitly requested @@ -652,7 +761,12 @@ pub(crate) unsafe fn path_readlink( let mut buf = dec_slice_of_mut_u8(memory, buf_ptr, buf_len)?; - let host_bufused = hostcalls_impl::path_readlink(resolved, &mut buf)?; + let host_bufused = match resolved.dirfd() { + Descriptor::VirtualFile(_virt) => { + unimplemented!("virtual readlink"); + } + _ => hostcalls_impl::path_readlink(resolved, &mut buf)?, + }; trace!(" | (buf_ptr,*buf_used)={:?}", buf); trace!(" | *buf_used={:?}", host_bufused); @@ -708,7 +822,15 @@ pub(crate) unsafe fn path_rename( log::debug!("path_rename resolved_old={:?}", resolved_old); log::debug!("path_rename resolved_new={:?}", resolved_new); - hostcalls_impl::path_rename(resolved_old, resolved_new) + if let (Descriptor::OsHandle(_), Descriptor::OsHandle(_)) = + (resolved_old.dirfd(), resolved_new.dirfd()) + { + hostcalls_impl::path_rename(resolved_old, resolved_new) + } else { + // Virtual files do not support rename, at the moment, and streams don't have paths to + // rename, so any combination of Descriptor that gets here is an error in the making. + panic!("path_rename with one or more non-OS files"); + } } pub(crate) unsafe fn fd_filestat_get( @@ -727,7 +849,15 @@ pub(crate) unsafe fn fd_filestat_get( .get_fd_entry(fd)? .as_descriptor(wasi::__WASI_RIGHTS_FD_FILESTAT_GET, 0)? .as_file()?; - let host_filestat = hostcalls_impl::fd_filestat_get(fd)?; + let host_filestat = match fd { + Descriptor::OsHandle(fd) => hostcalls_impl::fd_filestat_get(&fd)?, + Descriptor::VirtualFile(virt) => virt.filestat_get()?, + _ => { + unreachable!( + "implementation error: fd should have been checked to not be a stream already" + ); + } + }; trace!(" | *filestat_ptr={:?}", host_filestat); @@ -755,11 +885,11 @@ pub(crate) unsafe fn fd_filestat_set_times( .as_descriptor(wasi::__WASI_RIGHTS_FD_FILESTAT_SET_TIMES, 0)? .as_file()?; - fd_filestat_set_times_impl(fd, st_atim, st_mtim, fst_flags) + fd_filestat_set_times_impl(&fd, st_atim, st_mtim, fst_flags) } pub(crate) fn fd_filestat_set_times_impl( - fd: &File, + file: &Descriptor, st_atim: wasi::__wasi_timestamp_t, st_mtim: wasi::__wasi_timestamp_t, fst_flags: wasi::__wasi_fstflags_t, @@ -791,7 +921,15 @@ pub(crate) fn fd_filestat_set_times_impl( } else { None }; - set_file_handle_times(fd, atim, mtim).map_err(Into::into) + match file { + Descriptor::OsHandle(fd) => set_file_handle_times(fd, atim, mtim).map_err(Into::into), + Descriptor::VirtualFile(virt) => virt.filestat_set_times(atim, mtim), + _ => { + unreachable!( + "implementation error: fd should have been checked to not be a stream already" + ); + } + } } pub(crate) unsafe fn fd_filestat_set_size( @@ -802,7 +940,7 @@ pub(crate) unsafe fn fd_filestat_set_size( ) -> Result<()> { trace!("fd_filestat_set_size(fd={:?}, st_size={})", fd, st_size); - let fd = wasi_ctx + let file = wasi_ctx .get_fd_entry(fd)? .as_descriptor(wasi::__WASI_RIGHTS_FD_FILESTAT_SET_SIZE, 0)? .as_file()?; @@ -811,7 +949,15 @@ pub(crate) unsafe fn fd_filestat_set_size( if st_size > i64::max_value() as u64 { return Err(Error::E2BIG); } - fd.set_len(st_size).map_err(Into::into) + match file { + Descriptor::OsHandle(fd) => fd.set_len(st_size).map_err(Into::into), + Descriptor::VirtualFile(virt) => virt.filestat_set_size(st_size), + _ => { + unreachable!( + "implementation error: fd should have been checked to not be a stream already" + ); + } + } } pub(crate) unsafe fn path_filestat_get( @@ -845,7 +991,12 @@ pub(crate) unsafe fn path_filestat_get( path, false, )?; - let host_filestat = hostcalls_impl::path_filestat_get(resolved, dirflags)?; + let host_filestat = match resolved.dirfd() { + Descriptor::VirtualFile(virt) => virt + .openat(std::path::Path::new(resolved.path()), false, false, 0, 0)? + .filestat_get()?, + _ => hostcalls_impl::path_filestat_get(resolved, dirflags)?, + }; trace!(" | *filestat_ptr={:?}", host_filestat); @@ -887,7 +1038,14 @@ pub(crate) unsafe fn path_filestat_set_times( false, )?; - hostcalls_impl::path_filestat_set_times(resolved, dirflags, st_atim, st_mtim, fst_flags) + match resolved.dirfd() { + Descriptor::VirtualFile(_virt) => { + unimplemented!("virtual filestat_set_times"); + } + _ => { + hostcalls_impl::path_filestat_set_times(resolved, dirflags, st_atim, st_mtim, fst_flags) + } + } } pub(crate) unsafe fn path_symlink( @@ -917,7 +1075,12 @@ pub(crate) unsafe fn path_symlink( let fe = wasi_ctx.get_fd_entry(dirfd)?; let resolved_new = path_get(fe, wasi::__WASI_RIGHTS_PATH_SYMLINK, 0, 0, new_path, true)?; - hostcalls_impl::path_symlink(old_path, resolved_new) + match resolved_new.dirfd() { + Descriptor::VirtualFile(_virt) => { + unimplemented!("virtual path_symlink"); + } + _non_virtual => hostcalls_impl::path_symlink(old_path, resolved_new), + } } pub(crate) unsafe fn path_unlink_file( @@ -941,7 +1104,10 @@ pub(crate) unsafe fn path_unlink_file( let fe = wasi_ctx.get_fd_entry(dirfd)?; let resolved = path_get(fe, wasi::__WASI_RIGHTS_PATH_UNLINK_FILE, 0, 0, path, false)?; - hostcalls_impl::path_unlink_file(resolved) + match resolved.dirfd() { + Descriptor::VirtualFile(virt) => virt.unlink_file(resolved.path()), + _ => hostcalls_impl::path_unlink_file(resolved), + } } pub(crate) unsafe fn path_remove_directory( @@ -974,7 +1140,10 @@ pub(crate) unsafe fn path_remove_directory( log::debug!("path_remove_directory resolved={:?}", resolved); - hostcalls_impl::path_remove_directory(resolved) + match resolved.dirfd() { + Descriptor::VirtualFile(virt) => virt.remove_directory(resolved.path()), + _ => hostcalls_impl::path_remove_directory(resolved), + } } pub(crate) unsafe fn fd_prestat_get( @@ -1068,24 +1237,41 @@ pub(crate) unsafe fn fd_readdir( .get_fd_entry_mut(fd)? .as_descriptor_mut(wasi::__WASI_RIGHTS_FD_READDIR, 0)? .as_file_mut()?; - let mut host_buf = dec_slice_of_mut_u8(memory, buf, buf_len)?; + let host_buf = dec_slice_of_mut_u8(memory, buf, buf_len)?; trace!(" | (buf,buf_len)={:?}", host_buf); - let iter = hostcalls_impl::fd_readdir(file, cookie)?; - let mut host_bufused = 0; - for dirent in iter { - let dirent_raw = dirent?.to_wasi_raw()?; - let offset = dirent_raw.len(); - if host_buf.len() < offset { - break; - } else { - host_buf[0..offset].copy_from_slice(&dirent_raw); - host_bufused += offset; - host_buf = &mut host_buf[offset..]; + fn copy_entities>>( + iter: T, + mut host_buf: &mut [u8], + ) -> Result { + let mut host_bufused = 0; + for dirent in iter { + let dirent_raw = dirent?.to_wasi_raw()?; + let offset = dirent_raw.len(); + if host_buf.len() < offset { + break; + } else { + host_buf[0..offset].copy_from_slice(&dirent_raw); + host_bufused += offset; + host_buf = &mut host_buf[offset..]; + } } + Ok(host_bufused) } + let host_bufused = match file { + Descriptor::OsHandle(file) => { + copy_entities(hostcalls_impl::fd_readdir(file, cookie)?, host_buf)? + } + Descriptor::VirtualFile(virt) => copy_entities(virt.readdir(cookie)?, host_buf)?, + _ => { + unreachable!( + "implementation error: fd should have been checked to not be a stream already" + ); + } + }; + trace!(" | *buf_used={:?}", host_bufused); enc_usize_byref(memory, buf_used, host_bufused) diff --git a/crates/wasi-common/src/hostcalls_impl/fs_helpers.rs b/crates/wasi-common/src/hostcalls_impl/fs_helpers.rs index 23a06c23fc..0a72552e27 100644 --- a/crates/wasi-common/src/hostcalls_impl/fs_helpers.rs +++ b/crates/wasi-common/src/hostcalls_impl/fs_helpers.rs @@ -1,24 +1,100 @@ #![allow(non_camel_case_types)] +use crate::sys::fdentry_impl::OsHandle; use crate::sys::host_impl; use crate::sys::hostcalls_impl::fs_helpers::*; -use crate::{error::WasiError, fdentry::FdEntry, wasi, Error, Result}; -use std::fs::File; +use crate::{error::WasiError, fdentry::Descriptor, fdentry::FdEntry, wasi, Error, Result}; use std::path::{Component, Path}; #[derive(Debug)] pub(crate) struct PathGet { - dirfd: File, + dirfd: Descriptor, path: String, } impl PathGet { - pub(crate) fn dirfd(&self) -> &File { + pub(crate) fn dirfd(&self) -> &Descriptor { &self.dirfd } pub(crate) fn path(&self) -> &str { &self.path } + + pub(crate) fn path_create_directory(self) -> Result<()> { + match &self.dirfd { + Descriptor::OsHandle(file) => { + crate::sys::hostcalls_impl::path_create_directory(&file, &self.path) + } + Descriptor::VirtualFile(virt) => virt.create_directory(&Path::new(&self.path)), + other => { + panic!("invalid descriptor to create directory: {:?}", other); + } + } + } + + pub(crate) fn open_with( + self, + read: bool, + write: bool, + oflags: u16, + fs_flags: u16, + ) -> Result { + match &self.dirfd { + Descriptor::OsHandle(_) => { + crate::sys::hostcalls_impl::path_open(self, read, write, oflags, fs_flags) + .map_err(Into::into) + } + Descriptor::VirtualFile(virt) => virt + .openat(Path::new(&self.path), read, write, oflags, fs_flags) + .map(|file| Descriptor::VirtualFile(file)), + other => { + panic!("invalid descriptor to path_open: {:?}", other); + } + } + } +} + +struct PathRef<'a, 'b> { + dirfd: &'a Descriptor, + path: &'b str, +} + +impl<'a, 'b> PathRef<'a, 'b> { + fn new(dirfd: &'a Descriptor, path: &'b str) -> Self { + PathRef { dirfd, path } + } + + fn open(&self) -> Result { + match self.dirfd { + Descriptor::OsHandle(file) => Ok(Descriptor::OsHandle(OsHandle::from(openat( + &file, &self.path, + )?))), + Descriptor::VirtualFile(virt) => virt + .openat( + Path::new(&self.path), + false, + false, + wasi::__WASI_OFLAGS_DIRECTORY, + 0, + ) + .map(|file| Descriptor::VirtualFile(file)), + other => { + panic!("invalid descriptor for open: {:?}", other); + } + } + } + + fn readlink(&self) -> Result { + match self.dirfd { + Descriptor::OsHandle(file) => readlinkat(file, self.path), + Descriptor::VirtualFile(virt) => { + virt.readlinkat(Path::new(self.path)).map_err(Into::into) + } + other => { + panic!("invalid descriptor for readlink: {:?}", other); + } + } + } } /// Normalizes a path to ensure that the target path is located under the directory provided. @@ -112,7 +188,9 @@ pub(crate) fn path_get( } if !path_stack.is_empty() || (ends_with_slash && !needs_final_component) { - match openat(dir_stack.last().ok_or(Error::ENOTCAPABLE)?, &head) { + match PathRef::new(dir_stack.last().ok_or(Error::ENOTCAPABLE)?, &head) + .open() + { Ok(new_dir) => { dir_stack.push(new_dir); } @@ -125,10 +203,11 @@ pub(crate) fn path_get( // this with ENOTDIR because of the O_DIRECTORY flag. { // attempt symlink expansion - let mut link_path = readlinkat( + let mut link_path = PathRef::new( dir_stack.last().ok_or(Error::ENOTCAPABLE)?, &head, - )?; + ) + .readlink()?; symlink_expansions += 1; if symlink_expansions > MAX_SYMLINK_EXPANSIONS { @@ -159,7 +238,9 @@ pub(crate) fn path_get( { // if there's a trailing slash, or if `LOOKUP_SYMLINK_FOLLOW` is set, attempt // symlink expansion - match readlinkat(dir_stack.last().ok_or(Error::ENOTCAPABLE)?, &head) { + match PathRef::new(dir_stack.last().ok_or(Error::ENOTCAPABLE)?, &head) + .readlink() + { Ok(mut link_path) => { symlink_expansions += 1; if symlink_expansions > MAX_SYMLINK_EXPANSIONS { diff --git a/crates/wasi-common/src/lib.rs b/crates/wasi-common/src/lib.rs index 9e197fa74d..7e074eba3b 100644 --- a/crates/wasi-common/src/lib.rs +++ b/crates/wasi-common/src/lib.rs @@ -32,6 +32,8 @@ mod memory; pub mod old; mod sandboxed_tty_writer; mod sys; +mod virtfs; +pub use virtfs::{FileContents, VirtualDirEntry}; pub mod wasi; pub mod wasi32; diff --git a/crates/wasi-common/src/memory.rs b/crates/wasi-common/src/memory.rs index fa6752002f..4c9cb10844 100644 --- a/crates/wasi-common/src/memory.rs +++ b/crates/wasi-common/src/memory.rs @@ -207,7 +207,7 @@ pub(crate) fn dec_ciovec_slice( .iter() .map(|raw_iov| { let len = dec_usize(PrimInt::from_le(raw_iov.buf_len)); - let buf = PrimInt::from_le(raw_iov.buf); + let buf: u32 = PrimInt::from_le(raw_iov.buf); Ok(host::__wasi_ciovec_t { buf: dec_ptr(memory, buf, len)? as *const u8, buf_len: len, diff --git a/crates/wasi-common/src/sys/unix/fdentry_impl.rs b/crates/wasi-common/src/sys/unix/fdentry_impl.rs index 6efd681cb2..2ba4bd0c13 100644 --- a/crates/wasi-common/src/sys/unix/fdentry_impl.rs +++ b/crates/wasi-common/src/sys/unix/fdentry_impl.rs @@ -11,6 +11,7 @@ impl AsRawFd for Descriptor { fn as_raw_fd(&self) -> RawFd { match self { Self::OsHandle(file) => file.as_raw_fd(), + Self::VirtualFile(_) => panic!("virtual files do not have a raw fd"), Self::Stdin => io::stdin().as_raw_fd(), Self::Stdout => io::stdout().as_raw_fd(), Self::Stderr => io::stderr().as_raw_fd(), diff --git a/crates/wasi-common/src/sys/unix/hostcalls_impl/fs.rs b/crates/wasi-common/src/sys/unix/hostcalls_impl/fs.rs index 255d82ec9a..ea01628b87 100644 --- a/crates/wasi-common/src/sys/unix/hostcalls_impl/fs.rs +++ b/crates/wasi-common/src/sys/unix/hostcalls_impl/fs.rs @@ -1,5 +1,6 @@ #![allow(non_camel_case_types)] #![allow(unused_unsafe)] +use crate::fdentry::Descriptor; use crate::host::Dirent; use crate::hostcalls_impl::PathGet; use crate::sys::{fdentry_impl::OsHandle, host_impl, unix::sys_impl}; @@ -60,16 +61,9 @@ pub(crate) fn fd_advise( unsafe { posix_fadvise(file.as_raw_fd(), offset, len, host_advice) }.map_err(Into::into) } -pub(crate) fn path_create_directory(resolved: PathGet) -> Result<()> { +pub(crate) fn path_create_directory(base: &File, path: &str) -> Result<()> { use yanix::file::{mkdirat, Mode}; - unsafe { - mkdirat( - resolved.dirfd().as_raw_fd(), - resolved.path(), - Mode::from_bits_truncate(0o777), - ) - } - .map_err(Into::into) + unsafe { mkdirat(base.as_raw_fd(), path, Mode::from_bits_truncate(0o777)) }.map_err(Into::into) } pub(crate) fn path_link(resolved_old: PathGet, resolved_new: PathGet) -> Result<()> { @@ -92,7 +86,7 @@ pub(crate) fn path_open( write: bool, oflags: wasi::__wasi_oflags_t, fs_flags: wasi::__wasi_fdflags_t, -) -> Result { +) -> Result { use yanix::file::{fstatat, openat, AtFlag, FileType, Mode, OFlag}; let mut nix_all_oflags = if read && write { @@ -119,14 +113,15 @@ pub(crate) fn path_open( log::debug!("path_open resolved = {:?}", resolved); log::debug!("path_open oflags = {:?}", nix_all_oflags); - let new_fd = match unsafe { + let fd_no = unsafe { openat( resolved.dirfd().as_raw_fd(), resolved.path(), nix_all_oflags, Mode::from_bits_truncate(0o666), ) - } { + }; + let new_fd = match fd_no { Ok(fd) => fd, Err(e) => { if let yanix::Error::Io(ref err) = e { @@ -188,7 +183,7 @@ pub(crate) fn path_open( log::debug!("path_open (host) new_fd = {:?}", new_fd); // Determine the type of the new file descriptor and which rights contradict with this type - Ok(unsafe { File::from_raw_fd(new_fd) }) + Ok(OsHandle::from(unsafe { File::from_raw_fd(new_fd) }).into()) } pub(crate) fn path_readlink(resolved: PathGet, buf: &mut [u8]) -> Result { @@ -263,7 +258,7 @@ pub(crate) fn path_filestat_set_times( }; utimensat( - resolved.dirfd(), + &resolved.dirfd().as_os_handle(), resolved.path(), atim, mtim, @@ -274,6 +269,7 @@ pub(crate) fn path_filestat_set_times( pub(crate) fn path_remove_directory(resolved: PathGet) -> Result<()> { use yanix::file::{unlinkat, AtFlag}; + unsafe { unlinkat( resolved.dirfd().as_raw_fd(), diff --git a/crates/wasi-common/src/sys/unix/linux/hostcalls_impl.rs b/crates/wasi-common/src/sys/unix/linux/hostcalls_impl.rs index 0ddac0f41a..308c085d4c 100644 --- a/crates/wasi-common/src/sys/unix/linux/hostcalls_impl.rs +++ b/crates/wasi-common/src/sys/unix/linux/hostcalls_impl.rs @@ -1,3 +1,4 @@ +use crate::fdentry::Descriptor; use crate::hostcalls_impl::PathGet; use crate::Result; use std::os::unix::prelude::AsRawFd; @@ -26,15 +27,22 @@ pub(crate) fn path_symlink(old_path: &str, resolved: PathGet) -> Result<()> { pub(crate) fn path_rename(resolved_old: PathGet, resolved_new: PathGet) -> Result<()> { use yanix::file::renameat; - unsafe { - renameat( - resolved_old.dirfd().as_raw_fd(), - resolved_old.path(), - resolved_new.dirfd().as_raw_fd(), - resolved_new.path(), - ) + match (resolved_old.dirfd(), resolved_new.dirfd()) { + (Descriptor::OsHandle(resolved_old_file), Descriptor::OsHandle(resolved_new_file)) => { + unsafe { + renameat( + resolved_old_file.as_raw_fd(), + resolved_old.path(), + resolved_new_file.as_raw_fd(), + resolved_new.path(), + ) + } + .map_err(Into::into) + } + _ => { + unimplemented!("path_link with one or more virtual files"); + } } - .map_err(Into::into) } pub(crate) mod fd_readdir_impl { diff --git a/crates/wasi-common/src/sys/windows/fdentry_impl.rs b/crates/wasi-common/src/sys/windows/fdentry_impl.rs index 8700638004..bd1d1e88cf 100644 --- a/crates/wasi-common/src/sys/windows/fdentry_impl.rs +++ b/crates/wasi-common/src/sys/windows/fdentry_impl.rs @@ -39,6 +39,9 @@ impl AsRawHandle for Descriptor { fn as_raw_handle(&self) -> RawHandle { match self { Self::OsHandle(file) => file.as_raw_handle(), + Self::VirtualFile(_file) => { + unimplemented!("virtual as_raw_handle"); + } Self::Stdin => io::stdin().as_raw_handle(), Self::Stdout => io::stdout().as_raw_handle(), Self::Stderr => io::stderr().as_raw_handle(), diff --git a/crates/wasi-common/src/sys/windows/hostcalls_impl/fs.rs b/crates/wasi-common/src/sys/windows/hostcalls_impl/fs.rs index 1baacb5e05..b8044d1bd5 100644 --- a/crates/wasi-common/src/sys/windows/hostcalls_impl/fs.rs +++ b/crates/wasi-common/src/sys/windows/hostcalls_impl/fs.rs @@ -2,7 +2,7 @@ #![allow(unused)] use super::fs_helpers::*; use crate::ctx::WasiCtx; -use crate::fdentry::FdEntry; +use crate::fdentry::{Descriptor, FdEntry}; use crate::host::{Dirent, FileType}; use crate::hostcalls_impl::{fd_filestat_set_times_impl, PathGet}; use crate::sys::fdentry_impl::{determine_type_rights, OsHandle}; @@ -119,8 +119,8 @@ pub(crate) fn fd_advise( Ok(()) } -pub(crate) fn path_create_directory(resolved: PathGet) -> Result<()> { - let path = resolved.concatenate()?; +pub(crate) fn path_create_directory(file: &File, path: &str) -> Result<()> { + let path = concatenate(file, path)?; std::fs::create_dir(&path).map_err(Into::into) } @@ -134,7 +134,7 @@ pub(crate) fn path_open( write: bool, oflags: wasi::__wasi_oflags_t, fdflags: wasi::__wasi_fdflags_t, -) -> Result { +) -> Result { use winx::file::{AccessMode, CreationDisposition, Flags}; let is_trunc = oflags & wasi::__WASI_OFLAGS_TRUNC != 0; @@ -207,6 +207,7 @@ pub(crate) fn path_open( opts.access_mode(access_mode.bits()) .custom_flags(file_flags_from_fdflags(fdflags).bits()) .open(&path) + .map(|f| OsHandle::from(f).into()) .map_err(Into::into) } @@ -371,7 +372,7 @@ pub(crate) fn path_readlink(resolved: PathGet, buf: &mut [u8]) -> Result // we need to strip the prefix from the absolute path // as otherwise we will error out since WASI is not capable // of dealing with absolute paths - let dir_path = get_file_path(resolved.dirfd())?; + let dir_path = get_file_path(&resolved.dirfd().as_os_handle())?; let dir_path = PathBuf::from(strip_extended_prefix(dir_path)); let target_path = target_path .strip_prefix(dir_path) @@ -401,7 +402,7 @@ pub(crate) fn path_readlink(resolved: PathGet, buf: &mut [u8]) -> Result fn strip_trailing_slashes_and_concatenate(resolved: &PathGet) -> Result> { if resolved.path().ends_with('/') { let suffix = resolved.path().trim_end_matches('/'); - concatenate(resolved.dirfd(), Path::new(suffix)).map(Some) + concatenate(&resolved.dirfd().as_os_handle(), Path::new(suffix)).map(Some) } else { Ok(None) } @@ -492,14 +493,15 @@ pub(crate) fn path_filestat_set_times( let file = OpenOptions::new() .access_mode(AccessMode::FILE_WRITE_ATTRIBUTES.bits()) .open(path)?; - fd_filestat_set_times_impl(&file, st_atim, st_mtim, fst_flags) + let modifiable_fd = Descriptor::OsHandle(OsHandle::from(file)); + fd_filestat_set_times_impl(&modifiable_fd, st_atim, st_mtim, fst_flags) } pub(crate) fn path_symlink(old_path: &str, resolved: PathGet) -> Result<()> { use std::os::windows::fs::{symlink_dir, symlink_file}; use winx::winerror::WinError; - let old_path = concatenate(resolved.dirfd(), Path::new(old_path))?; + let old_path = concatenate(&resolved.dirfd().as_os_handle(), Path::new(old_path))?; let new_path = resolved.concatenate()?; // try creating a file symlink diff --git a/crates/wasi-common/src/sys/windows/hostcalls_impl/fs_helpers.rs b/crates/wasi-common/src/sys/windows/hostcalls_impl/fs_helpers.rs index 8fe392ccba..c390596436 100644 --- a/crates/wasi-common/src/sys/windows/hostcalls_impl/fs_helpers.rs +++ b/crates/wasi-common/src/sys/windows/hostcalls_impl/fs_helpers.rs @@ -1,4 +1,5 @@ #![allow(non_camel_case_types)] +use crate::fdentry::Descriptor; use crate::hostcalls_impl::PathGet; use crate::{wasi, Error, Result}; use std::ffi::{OsStr, OsString}; @@ -12,7 +13,15 @@ pub(crate) trait PathGetExt { impl PathGetExt for PathGet { fn concatenate(&self) -> Result { - concatenate(self.dirfd(), Path::new(self.path())) + match self.dirfd() { + Descriptor::OsHandle(file) => concatenate(file, Path::new(self.path())), + Descriptor::VirtualFile(_virt) => { + panic!("concatenate on a virtual base"); + } + Descriptor::Stdin | Descriptor::Stdout | Descriptor::Stderr => { + unreachable!("streams do not have paths and should not be accessible via PathGet"); + } + } } } @@ -126,7 +135,7 @@ pub(crate) fn strip_extended_prefix>(path: P) -> OsString { } } -pub(crate) fn concatenate>(dirfd: &File, path: P) -> Result { +pub(crate) fn concatenate>(file: &File, path: P) -> Result { use winx::file::get_file_path; // WASI is not able to deal with absolute paths @@ -135,7 +144,7 @@ pub(crate) fn concatenate>(dirfd: &File, path: P) -> Result Ok(1), // On Unix, ioctl(FIONREAD) will return 0 for stdout/stderr. Emulate the same behavior on Windows. Descriptor::Stdout | Descriptor::Stderr => Ok(0), + Descriptor::VirtualFile(_) => { + panic!("virtual files do not get rw events"); + } }; let new_event = make_rw_event(&event, size); @@ -295,6 +298,9 @@ pub(crate) fn poll_oneoff( unreachable!(); } } + Descriptor::VirtualFile(_) => { + panic!("virtual files do not get rw events"); + } } } diff --git a/crates/wasi-common/src/virtfs.rs b/crates/wasi-common/src/virtfs.rs new file mode 100644 index 0000000000..52e48629aa --- /dev/null +++ b/crates/wasi-common/src/virtfs.rs @@ -0,0 +1,878 @@ +use crate::host::Dirent; +use crate::host::FileType; +use crate::{wasi, wasi32, Error, Result}; +use filetime::FileTime; +use log::trace; +use std::cell::RefCell; +use std::collections::hash_map::Entry; +use std::collections::HashMap; +use std::convert::TryInto; +use std::io; +use std::io::SeekFrom; +use std::path::{Path, PathBuf}; +use std::rc::Rc; + +pub enum VirtualDirEntry { + Directory(HashMap), + File(Box), +} + +impl VirtualDirEntry { + pub fn empty_directory() -> Self { + VirtualDirEntry::Directory(HashMap::new()) + } +} + +/// Files and directories may be moved, and for implementation reasons retain a reference to their +/// parent VirtualFile, so files that can be moved must provide an interface to update their parent +/// reference. +pub(crate) trait MovableFile { + fn set_parent(&self, new_parent: Option>); +} + +/// `VirtualFile` encompasses the whole interface of a `File`, `Directory`, or `Stream`-like +/// object, suitable for forwarding from `wasi-common` public interfaces. `File` and +/// `Directory`-style objects can be moved, so implemetors of this trait must also implement +/// `MovableFile`. +/// +/// Default implementations of functions here fail in ways that are intended to mimic a file-like +/// object with no permissions, no content, and that cannot be used in any way. +pub(crate) trait VirtualFile: MovableFile { + fn fdstat_get(&self) -> wasi::__wasi_fdflags_t { + 0 + } + + fn try_clone(&self) -> io::Result>; + + fn readlinkat(&self, _path: &Path) -> Result { + Err(Error::EACCES) + } + + fn openat( + &self, + _path: &Path, + _read: bool, + _write: bool, + _oflags: wasi::__wasi_oflags_t, + _fd_flags: wasi::__wasi_fdflags_t, + ) -> Result> { + Err(Error::EACCES) + } + + fn remove_directory(&self, _path: &str) -> Result<()> { + Err(Error::EACCES) + } + + fn unlink_file(&self, _path: &str) -> Result<()> { + Err(Error::EACCES) + } + + fn datasync(&self) -> Result<()> { + Err(Error::EINVAL) + } + + fn sync(&self) -> Result<()> { + Ok(()) + } + + fn create_directory(&self, _path: &Path) -> Result<()> { + Err(Error::EACCES) + } + + fn readdir( + &self, + _cookie: wasi::__wasi_dircookie_t, + ) -> Result>>> { + Err(Error::EBADF) + } + + fn write_vectored(&mut self, _iovs: &[io::IoSlice]) -> Result { + Err(Error::EBADF) + } + + fn pread(&self, _buf: &mut [u8], _offset: u64) -> Result { + Err(Error::EBADF) + } + + fn pwrite(&self, _buf: &mut [u8], _offset: u64) -> Result { + Err(Error::EBADF) + } + + fn seek(&mut self, _offset: SeekFrom) -> Result { + Err(Error::EBADF) + } + + fn advise( + &self, + _advice: wasi::__wasi_advice_t, + _offset: wasi::__wasi_filesize_t, + _len: wasi::__wasi_filesize_t, + ) -> Result<()> { + Err(Error::EBADF) + } + + fn allocate( + &self, + _offset: wasi::__wasi_filesize_t, + _len: wasi::__wasi_filesize_t, + ) -> Result<()> { + Err(Error::EBADF) + } + + fn filestat_get(&self) -> Result { + Err(Error::EBADF) + } + + fn filestat_set_times(&self, _atim: Option, _mtim: Option) -> Result<()> { + Err(Error::EBADF) + } + + fn filestat_set_size(&self, _st_size: wasi::__wasi_filesize_t) -> Result<()> { + Err(Error::EBADF) + } + + fn fdstat_set_flags(&mut self, _fdflags: wasi::__wasi_fdflags_t) -> Result<()> { + Err(Error::EBADF) + } + + fn read_vectored(&mut self, _iovs: &mut [io::IoSliceMut]) -> Result { + Err(Error::EBADF) + } + + fn get_file_type(&self) -> wasi::__wasi_filetype_t; + + fn get_rights_base(&self) -> wasi::__wasi_rights_t { + 0 + } + + fn get_rights_inheriting(&self) -> wasi::__wasi_rights_t { + 0 + } +} + +pub trait FileContents { + /// The implementation-defined maximum size of the store corresponding to a `FileContents` + /// implementation. + fn max_size(&self) -> wasi::__wasi_filesize_t; + /// The current number of bytes this `FileContents` describes. + fn size(&self) -> wasi::__wasi_filesize_t; + /// Resize to hold `new_size` number of bytes, or error if this is not possible. + fn resize(&mut self, new_size: wasi::__wasi_filesize_t) -> Result<()>; + /// Write a list of `IoSlice` starting at `offset`. `offset` plus the total size of all `iovs` + /// is guaranteed to not exceed `max_size`. Implementations must not indicate more bytes have + /// been written than can be held by `iovs`. + fn pwritev(&mut self, iovs: &[io::IoSlice], offset: wasi::__wasi_filesize_t) -> Result; + /// Read from the file from `offset`, filling a list of `IoSlice`. The returend size must not + /// be more than the capactiy of `iovs`, and must not exceed the limit reported by + /// `self.max_size()`. + fn preadv(&self, iovs: &mut [io::IoSliceMut], offset: wasi::__wasi_filesize_t) + -> Result; + /// Write contents from `buf` to this file starting at `offset`. `offset` plus the length of + /// `buf` is guaranteed to not exceed `max_size`. Implementations must not indicate more bytes + /// have been written than the size of `buf`. + fn pwrite(&mut self, buf: &[u8], offset: wasi::__wasi_filesize_t) -> Result; + /// Read from the file at `offset`, filling `buf`. The returned size must not be more than the + /// capacity of `buf`, and `offset` plus the returned size must not exceed `self.max_size()`. + fn pread(&self, buf: &mut [u8], offset: wasi::__wasi_filesize_t) -> Result; +} + +impl FileContents for VecFileContents { + fn max_size(&self) -> wasi::__wasi_filesize_t { + std::usize::MAX as wasi::__wasi_filesize_t + } + + fn size(&self) -> wasi::__wasi_filesize_t { + self.content.len() as wasi::__wasi_filesize_t + } + + fn resize(&mut self, new_size: wasi::__wasi_filesize_t) -> Result<()> { + let new_size: usize = new_size.try_into().map_err(|_| Error::EINVAL)?; + self.content.resize(new_size, 0); + Ok(()) + } + + fn preadv( + &self, + iovs: &mut [io::IoSliceMut], + offset: wasi::__wasi_filesize_t, + ) -> Result { + let mut read_total = 0usize; + for iov in iovs.iter_mut() { + let read = self.pread(iov, offset)?; + read_total = read_total.checked_add(read).expect("FileContents::preadv must not be called when reads could total to more bytes than the return value can hold"); + } + Ok(read_total) + } + + fn pwritev(&mut self, iovs: &[io::IoSlice], offset: wasi::__wasi_filesize_t) -> Result { + let mut write_total = 0usize; + for iov in iovs.iter() { + let written = self.pwrite(iov, offset)?; + write_total = write_total.checked_add(written).expect("FileContents::pwritev must not be called when writes could total to more bytes than the return value can hold"); + } + Ok(write_total) + } + + fn pread(&self, buf: &mut [u8], offset: wasi::__wasi_filesize_t) -> Result { + trace!(" | pread(buf.len={}, offset={})", buf.len(), offset); + let offset: usize = offset.try_into().map_err(|_| Error::EINVAL)?; + + let data_remaining = self.content.len().saturating_sub(offset); + + let read_count = std::cmp::min(buf.len(), data_remaining); + + (&mut buf[..read_count]).copy_from_slice(&self.content[offset..][..read_count]); + + let res = Ok(read_count); + trace!(" | pread={:?}", res); + res + } + + fn pwrite(&mut self, buf: &[u8], offset: wasi::__wasi_filesize_t) -> Result { + let offset: usize = offset.try_into().map_err(|_| Error::EINVAL)?; + + let write_end = offset.checked_add(buf.len()).ok_or(Error::EFBIG)?; + + if write_end > self.content.len() { + self.content.resize(write_end, 0); + } + + (&mut self.content[offset..][..buf.len()]).copy_from_slice(buf); + + Ok(buf.len()) + } +} + +struct VecFileContents { + content: Vec, +} + +impl VecFileContents { + fn new() -> Self { + Self { + content: Vec::new(), + } + } +} + +/// An `InMemoryFile` is a shared handle to some underlying data. The relationship is analagous to +/// a filesystem wherein a file descriptor is one view into a possibly-shared underlying collection +/// of data and permissions on a filesystem. +pub struct InMemoryFile { + cursor: wasi::__wasi_filesize_t, + parent: Rc>>>, + fd_flags: wasi::__wasi_fdflags_t, + data: Rc>>, +} + +impl InMemoryFile { + pub fn memory_backed() -> Self { + Self { + cursor: 0, + parent: Rc::new(RefCell::new(None)), + fd_flags: 0, + data: Rc::new(RefCell::new(Box::new(VecFileContents::new()))), + } + } + + pub fn new(contents: Box) -> Self { + Self { + cursor: 0, + fd_flags: 0, + parent: Rc::new(RefCell::new(None)), + data: Rc::new(RefCell::new(contents)), + } + } +} + +impl MovableFile for InMemoryFile { + fn set_parent(&self, new_parent: Option>) { + *self.parent.borrow_mut() = new_parent; + } +} + +impl VirtualFile for InMemoryFile { + fn fdstat_get(&self) -> wasi::__wasi_fdflags_t { + self.fd_flags + } + + fn try_clone(&self) -> io::Result> { + Ok(Box::new(InMemoryFile { + cursor: 0, + fd_flags: self.fd_flags, + parent: Rc::clone(&self.parent), + data: Rc::clone(&self.data), + })) + } + + fn readlinkat(&self, _path: &Path) -> Result { + // no symlink support, so always say it's invalid. + Err(Error::ENOTDIR) + } + + fn openat( + &self, + path: &Path, + read: bool, + write: bool, + oflags: wasi::__wasi_oflags_t, + fd_flags: wasi::__wasi_fdflags_t, + ) -> Result> { + log::trace!( + "InMemoryFile::openat(path={:?}, read={:?}, write={:?}, oflags={:?}, fd_flags={:?}", + path, + read, + write, + oflags, + fd_flags + ); + + if oflags & wasi::__WASI_OFLAGS_DIRECTORY != 0 { + log::trace!( + "InMemoryFile::openat was passed oflags DIRECTORY, but {:?} is a file.", + path + ); + log::trace!(" return ENOTDIR"); + return Err(Error::ENOTDIR); + } + + if path == Path::new(".") { + return self.try_clone().map_err(Into::into); + } else if path == Path::new("..") { + match &*self.parent.borrow() { + Some(file) => file.try_clone().map_err(Into::into), + None => self.try_clone().map_err(Into::into), + } + } else { + Err(Error::EACCES) + } + } + + fn remove_directory(&self, _path: &str) -> Result<()> { + Err(Error::ENOTDIR) + } + + fn unlink_file(&self, _path: &str) -> Result<()> { + Err(Error::ENOTDIR) + } + + fn fdstat_set_flags(&mut self, fdflags: wasi::__wasi_fdflags_t) -> Result<()> { + self.fd_flags = fdflags; + Ok(()) + } + + fn write_vectored(&mut self, iovs: &[io::IoSlice]) -> Result { + trace!("write_vectored(iovs={:?})", iovs); + let mut data = self.data.borrow_mut(); + + let append_mode = self.fd_flags & wasi::__WASI_FDFLAGS_APPEND != 0; + trace!(" | fd_flags={:o}", self.fd_flags); + + // If this file is in append mode, we write to the end. + let write_start = if append_mode { + data.size() + } else { + self.cursor + }; + + let max_size = iovs + .iter() + .map(|iov| { + let cast_iovlen: wasi32::size_t = iov + .len() + .try_into() + .expect("iovec are bounded by wasi max sizes"); + cast_iovlen + }) + .fold(Some(0u32), |len, iov| len.and_then(|x| x.checked_add(iov))) + .expect("write_vectored will not be called with invalid iovs"); + + if let Some(end) = write_start.checked_add(max_size as wasi::__wasi_filesize_t) { + if end > data.max_size() { + return Err(Error::EFBIG); + } + } else { + return Err(Error::EFBIG); + } + + trace!(" | *write_start={:?}", write_start); + let written = data.pwritev(iovs, write_start)?; + + // If we are not appending, adjust the cursor appropriately for the write, too. This can't + // overflow, as we checked against that before writing any data. + if !append_mode { + self.cursor += written as u64; + } + + Ok(written) + } + + fn read_vectored(&mut self, iovs: &mut [io::IoSliceMut]) -> Result { + trace!("read_vectored(iovs={:?})", iovs); + trace!(" | *read_start={:?}", self.cursor); + self.data.borrow_mut().preadv(iovs, self.cursor) + } + + fn pread(&self, buf: &mut [u8], offset: wasi::__wasi_filesize_t) -> Result { + self.data.borrow_mut().pread(buf, offset) + } + + fn pwrite(&self, buf: &mut [u8], offset: wasi::__wasi_filesize_t) -> Result { + self.data.borrow_mut().pwrite(buf, offset) + } + + fn seek(&mut self, offset: SeekFrom) -> Result { + let content_len = self.data.borrow().size(); + match offset { + SeekFrom::Current(offset) => { + let new_cursor = if offset < 0 { + self.cursor + .checked_sub(offset.wrapping_neg() as u64) + .ok_or(Error::EINVAL)? + } else { + self.cursor + .checked_add(offset as u64) + .ok_or(Error::EINVAL)? + }; + self.cursor = std::cmp::min(content_len, new_cursor); + } + SeekFrom::End(offset) => { + // A negative offset from the end would be past the end of the file, + let offset: u64 = offset.try_into().map_err(|_| Error::EINVAL)?; + self.cursor = content_len.saturating_sub(offset); + } + SeekFrom::Start(offset) => { + // A negative offset from the end would be before the start of the file. + let offset: u64 = offset.try_into().map_err(|_| Error::EINVAL)?; + self.cursor = std::cmp::min(content_len, offset); + } + } + + Ok(self.cursor) + } + + fn advise( + &self, + advice: wasi::__wasi_advice_t, + _offset: wasi::__wasi_filesize_t, + _len: wasi::__wasi_filesize_t, + ) -> Result<()> { + // we'll just ignore advice for now, unless it's totally invalid + match advice { + wasi::__WASI_ADVICE_DONTNEED + | wasi::__WASI_ADVICE_SEQUENTIAL + | wasi::__WASI_ADVICE_WILLNEED + | wasi::__WASI_ADVICE_NOREUSE + | wasi::__WASI_ADVICE_RANDOM + | wasi::__WASI_ADVICE_NORMAL => Ok(()), + _ => Err(Error::EINVAL), + } + } + + fn allocate( + &self, + offset: wasi::__wasi_filesize_t, + len: wasi::__wasi_filesize_t, + ) -> Result<()> { + let new_limit = offset.checked_add(len).ok_or(Error::EFBIG)?; + let mut data = self.data.borrow_mut(); + + if new_limit > data.max_size() { + return Err(Error::EFBIG); + } + + if new_limit > data.size() { + data.resize(new_limit)?; + } + + Ok(()) + } + + fn filestat_set_size(&self, st_size: wasi::__wasi_filesize_t) -> Result<()> { + let mut data = self.data.borrow_mut(); + if st_size > data.max_size() { + return Err(Error::EFBIG); + } + data.resize(st_size) + } + + fn filestat_get(&self) -> Result { + let stat = wasi::__wasi_filestat_t { + dev: 0, + ino: 0, + nlink: 0, + size: self.data.borrow().size(), + atim: 0, + ctim: 0, + mtim: 0, + filetype: self.get_file_type(), + }; + Ok(stat) + } + + fn get_file_type(&self) -> wasi::__wasi_filetype_t { + wasi::__WASI_FILETYPE_REGULAR_FILE + } + + fn get_rights_base(&self) -> wasi::__wasi_rights_t { + wasi::RIGHTS_REGULAR_FILE_BASE + } + + fn get_rights_inheriting(&self) -> wasi::__wasi_rights_t { + wasi::RIGHTS_REGULAR_FILE_INHERITING + } +} + +/// A clonable read/write directory. +pub struct VirtualDir { + writable: bool, + // All copies of this `VirtualDir` must share `parent`, and changes in one copy's `parent` + // must be reflected in all handles, so they share `Rc` of an underlying `parent`. + parent: Rc>>>, + entries: Rc>>>, +} + +impl VirtualDir { + pub fn new(writable: bool) -> Self { + VirtualDir { + writable, + parent: Rc::new(RefCell::new(None)), + entries: Rc::new(RefCell::new(HashMap::new())), + } + } + + #[allow(dead_code)] + pub fn with_dir>(mut self, dir: VirtualDir, path: P) -> Self { + self.add_dir(dir, path); + self + } + + #[allow(dead_code)] + pub fn add_dir>(&mut self, dir: VirtualDir, path: P) { + let entry = Box::new(dir); + entry.set_parent(Some(self.try_clone().expect("can clone self"))); + self.entries + .borrow_mut() + .insert(path.as_ref().to_owned(), entry); + } + + #[allow(dead_code)] + pub fn with_file>(mut self, content: Box, path: P) -> Self { + self.add_file(content, path); + self + } + + #[allow(dead_code)] + pub fn add_file>(&mut self, content: Box, path: P) { + let entry = Box::new(InMemoryFile::new(content)); + entry.set_parent(Some(self.try_clone().expect("can clone self"))); + self.entries + .borrow_mut() + .insert(path.as_ref().to_owned(), entry); + } +} + +impl MovableFile for VirtualDir { + fn set_parent(&self, new_parent: Option>) { + *self.parent.borrow_mut() = new_parent; + } +} + +const SELF_DIR_COOKIE: u32 = 0; +const PARENT_DIR_COOKIE: u32 = 1; + +// This MUST be the number of constants above. This limit is used to prevent allocation of files +// that would wrap and be mapped to the same dir cookies as `self` or `parent`. +const RESERVED_ENTRY_COUNT: u32 = 2; + +impl VirtualFile for VirtualDir { + fn try_clone(&self) -> io::Result> { + Ok(Box::new(VirtualDir { + writable: self.writable, + parent: Rc::clone(&self.parent), + entries: Rc::clone(&self.entries), + })) + } + + fn readlinkat(&self, _path: &Path) -> Result { + // Files are not symbolic links or directories, faithfully report ENOTDIR. + Err(Error::ENOTDIR) + } + + fn openat( + &self, + path: &Path, + read: bool, + write: bool, + oflags: wasi::__wasi_oflags_t, + fd_flags: wasi::__wasi_fdflags_t, + ) -> Result> { + log::trace!( + "VirtualDir::openat(path={:?}, read={:?}, write={:?}, oflags={:?}, fd_flags={:?}", + path, + read, + write, + oflags, + fd_flags + ); + + if path == Path::new(".") { + return self.try_clone().map_err(Into::into); + } else if path == Path::new("..") { + match &*self.parent.borrow() { + Some(file) => { + return file.try_clone().map_err(Into::into); + } + None => { + return self.try_clone().map_err(Into::into); + } + } + } + + // openat may have been passed a path with a trailing slash, but files are mapped to paths + // with trailing slashes normalized out. + let file_name = path.file_name().ok_or(Error::EINVAL)?; + let mut entries = self.entries.borrow_mut(); + let entry_count = entries.len(); + match entries.entry(Path::new(file_name).to_path_buf()) { + Entry::Occupied(e) => { + let creat_excl_mask = wasi::__WASI_OFLAGS_CREAT | wasi::__WASI_OFLAGS_EXCL; + if (oflags & creat_excl_mask) == creat_excl_mask { + log::trace!("VirtualDir::openat was passed oflags CREAT|EXCL, but the file {:?} exists.", file_name); + log::trace!(" return EEXIST"); + return Err(Error::EEXIST); + } + + if (oflags & wasi::__WASI_OFLAGS_DIRECTORY) != 0 + && e.get().get_file_type() != wasi::__WASI_FILETYPE_DIRECTORY + { + log::trace!( + "VirtualDir::openat was passed oflags DIRECTORY, but {:?} is a file.", + file_name + ); + log::trace!(" return ENOTDIR"); + return Err(Error::ENOTDIR); + } + + e.get().try_clone().map_err(Into::into) + } + Entry::Vacant(v) => { + if self.writable { + // Enforce a hard limit at `u32::MAX - 2` files. + // This is to have a constant limit (rather than target-dependent limit we + // would have with `usize`. The limit is the full `u32` range minus two so we + // can reserve "self" and "parent" cookie values. + if entry_count >= (std::u32::MAX - RESERVED_ENTRY_COUNT) as usize { + return Err(Error::ENOSPC); + } + + log::trace!( + "VirtualDir::openat creating an InMemoryFile named {}", + path.display() + ); + + let mut file = Box::new(InMemoryFile::memory_backed()); + file.fd_flags = fd_flags; + file.set_parent(Some(self.try_clone().expect("can clone self"))); + v.insert(file).try_clone().map_err(Into::into) + } else { + Err(Error::EACCES) + } + } + } + } + + fn remove_directory(&self, path: &str) -> Result<()> { + let trimmed_path = path.trim_end_matches('/'); + let mut entries = self.entries.borrow_mut(); + match entries.entry(Path::new(trimmed_path).to_path_buf()) { + Entry::Occupied(e) => { + // first, does this name a directory? + if e.get().get_file_type() != wasi::__WASI_FILETYPE_DIRECTORY { + return Err(Error::ENOTDIR); + } + + // Okay, but is the directory empty? + let iter = e.get().readdir(wasi::__WASI_DIRCOOKIE_START)?; + if iter.skip(RESERVED_ENTRY_COUNT as usize).next().is_some() { + return Err(Error::ENOTEMPTY); + } + + // Alright, it's an empty directory. We can remove it. + let removed = e.remove_entry(); + + // And sever the file's parent ref to avoid Rc cycles. + removed.1.set_parent(None); + + Ok(()) + } + Entry::Vacant(_) => { + log::trace!( + "VirtualDir::remove_directory failed to remove {}, no such entry", + trimmed_path + ); + Err(Error::ENOENT) + } + } + } + + fn unlink_file(&self, path: &str) -> Result<()> { + let trimmed_path = path.trim_end_matches('/'); + + // Special case: we may be unlinking this directory itself if path is `"."`. In that case, + // fail with EISDIR, since this is a directory. Alternatively, we may be unlinking `".."`, + // which is bound the same way, as this is by definition contained in a directory. + if trimmed_path == "." || trimmed_path == ".." { + return Err(Error::EISDIR); + } + + let mut entries = self.entries.borrow_mut(); + match entries.entry(Path::new(trimmed_path).to_path_buf()) { + Entry::Occupied(e) => { + // Directories must be removed through `remove_directory`, not `unlink_file`. + if e.get().get_file_type() == wasi::__WASI_FILETYPE_DIRECTORY { + return Err(Error::EISDIR); + } + + let removed = e.remove_entry(); + + // Sever the file's parent ref to avoid Rc cycles. + removed.1.set_parent(None); + + Ok(()) + } + Entry::Vacant(_) => { + log::trace!( + "VirtualDir::unlink_file failed to remove {}, no such entry", + trimmed_path + ); + Err(Error::ENOENT) + } + } + } + + fn create_directory(&self, path: &Path) -> Result<()> { + let mut entries = self.entries.borrow_mut(); + match entries.entry(path.to_owned()) { + Entry::Occupied(_) => Err(Error::EEXIST), + Entry::Vacant(v) => { + if self.writable { + let new_dir = Box::new(VirtualDir::new(true)); + new_dir.set_parent(Some(self.try_clone()?)); + v.insert(new_dir); + Ok(()) + } else { + Err(Error::EACCES) + } + } + } + } + + fn write_vectored(&mut self, _iovs: &[io::IoSlice]) -> Result { + Err(Error::EBADF) + } + + fn readdir( + &self, + cookie: wasi::__wasi_dircookie_t, + ) -> Result>>> { + struct VirtualDirIter { + start: u32, + entries: Rc>>>, + } + impl Iterator for VirtualDirIter { + type Item = Result; + + fn next(&mut self) -> Option { + log::trace!("VirtualDirIter::next continuing from {}", self.start); + if self.start == SELF_DIR_COOKIE { + self.start += 1; + return Some(Ok(Dirent { + name: ".".to_owned(), + ftype: FileType::from_wasi(wasi::__WASI_FILETYPE_DIRECTORY) + .expect("directories are valid file types"), + ino: 0, + cookie: self.start as u64, + })); + } + if self.start == PARENT_DIR_COOKIE { + self.start += 1; + return Some(Ok(Dirent { + name: "..".to_owned(), + ftype: FileType::from_wasi(wasi::__WASI_FILETYPE_DIRECTORY) + .expect("directories are valid file types"), + ino: 0, + cookie: self.start as u64, + })); + } + + let entries = self.entries.borrow(); + + // Adjust `start` to be an appropriate number of HashMap entries. + let start = self.start - RESERVED_ENTRY_COUNT; + if start as usize >= entries.len() { + return None; + } + + self.start += 1; + + let (path, file) = entries + .iter() + .skip(start as usize) + .next() + .expect("seeked less than the length of entries"); + + let entry = Dirent { + name: path + .to_str() + .expect("wasi paths are valid utf8 strings") + .to_owned(), + ftype: FileType::from_wasi(file.get_file_type()) + .expect("virtfs reports valid wasi file types"), + ino: 0, + cookie: self.start as u64, + }; + + Some(Ok(entry)) + } + } + let cookie = match cookie.try_into() { + Ok(cookie) => cookie, + Err(_) => { + // Cookie is larger than u32. it doesn't seem like there's an explicit error + // condition in POSIX or WASI, so just start from the start? + 0 + } + }; + Ok(Box::new(VirtualDirIter { + start: cookie, + entries: Rc::clone(&self.entries), + })) + } + + fn filestat_get(&self) -> Result { + let stat = wasi::__wasi_filestat_t { + dev: 0, + ino: 0, + nlink: 0, + size: 0, + atim: 0, + ctim: 0, + mtim: 0, + filetype: self.get_file_type(), + }; + Ok(stat) + } + + fn get_file_type(&self) -> wasi::__wasi_filetype_t { + wasi::__WASI_FILETYPE_DIRECTORY + } + + fn get_rights_base(&self) -> wasi::__wasi_rights_t { + wasi::RIGHTS_DIRECTORY_BASE + } + + fn get_rights_inheriting(&self) -> wasi::__wasi_rights_t { + wasi::RIGHTS_DIRECTORY_INHERITING + } +}