From cdfd9afccc4d591582d4240db825973855b36649 Mon Sep 17 00:00:00 2001 From: Mark McCaskey Date: Thu, 8 Aug 2019 11:59:35 +0900 Subject: [PATCH 1/3] Improve error reporting of IO errors, begin implementation of path_symlink --- lib/wasi/src/state/types.rs | 121 ++++++++++++++++++++++++++++++++--- lib/wasi/src/syscalls/mod.rs | 52 +++++++++++++-- 2 files changed, 156 insertions(+), 17 deletions(-) diff --git a/lib/wasi/src/state/types.rs b/lib/wasi/src/state/types.rs index 5390d6a8046..57ee29fdd6a 100644 --- a/lib/wasi/src/state/types.rs +++ b/lib/wasi/src/state/types.rs @@ -8,7 +8,7 @@ use std::{ }; /// Error type for external users -#[derive(Debug, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[allow(dead_code)] // dead code beacuse this is for external use pub enum WasiFsError { @@ -23,6 +23,38 @@ pub enum WasiFsError { /// Something failed when doing IO. These errors can generally not be handled. /// It may work if tried again. IOError, + /// The address was in use + AddressInUse, + /// The address could not be found + AddressNotAvailable, + /// A pipe was closed + BrokenPipe, + /// The connection was aborted + ConnectionAborted, + /// The connection request was refused + ConnectionRefused, + /// The connection was reset + ConnectionReset, + /// The operation was interrupted before it could finish + Interrupted, + /// Invalid internal data, if the argument data is invalid, use `InvalidInput` + InvalidData, + /// The provided data is invalid + InvalidInput, + /// Could not perform the operation because there was not an open connection + NotConnected, + /// The requested file or directory could not be found + EntityNotFound, + /// Caller was not allowed to perform this operation + PermissionDenied, + /// The operation did not complete within the given amount of time + TimedOut, + /// Found EOF when EOF was not expected + UnexpectedEof, + /// Operation would block, this error lets the caller know that they can try again + WouldBlock, + /// A call to write returned 0 + WriteZero, /// A WASI error without an external name. If you encounter this it means /// that there's probably a bug on our side (maybe as simple as forgetting to wrap /// this error, but perhaps something broke) @@ -35,9 +67,51 @@ impl WasiFsError { __WASI_EBADF => WasiFsError::InvalidFd, __WASI_EEXIST => WasiFsError::AlreadyExists, __WASI_EIO => WasiFsError::IOError, + __WASI_EADDRINUSE => WasiFsError::AddressInUse, + __WASI_EADDRNOTAVAIL => WasiFsError::AddressNotAvailable, + __WASI_EPIPE => WasiFsError::BrokenPipe, + __WASI_ECONNABORTED => WasiFsError::ConnectionAborted, + __WASI_ECONNREFUSED => WasiFsError::ConnectionRefused, + __WASI_ECONNRESET => WasiFsError::ConnectionReset, + __WASI_EINTR => WasiFsError::Interrupted, + __WASI_EINVAL => WasiFsError::InvalidInput, + __WASI_ENOTCONN => WasiFsError::NotConnected, + __WASI_ENOENT => WasiFsError::EntityNotFound, + __WASI_EPERM => WasiFsError::PermissionDenied, + __WASI_ETIMEDOUT => WasiFsError::TimedOut, + __WASI_EPROTO => WasiFsError::UnexpectedEof, + __WASI_EAGAIN => WasiFsError::WouldBlock, + __WASI_ENOSPC => WasiFsError::WriteZero, _ => WasiFsError::UnknownError(err), } } + + pub fn into_wasi_err(self) -> __wasi_errno_t { + match self { + WasiFsError::AlreadyExists => __WASI_EEXIST, + WasiFsError::AddressInUse => __WASI_EADDRINUSE, + WasiFsError::AddressNotAvailable => __WASI_EADDRNOTAVAIL, + WasiFsError::BaseNotDirectory => __WASI_ENOTDIR, + WasiFsError::BrokenPipe => __WASI_EPIPE, + WasiFsError::ConnectionAborted => __WASI_ECONNABORTED, + WasiFsError::ConnectionRefused => __WASI_ECONNREFUSED, + WasiFsError::ConnectionReset => __WASI_ECONNRESET, + WasiFsError::Interrupted => __WASI_EINTR, + WasiFsError::InvalidData => __WASI_EIO, + WasiFsError::InvalidFd => __WASI_EBADF, + WasiFsError::InvalidInput => __WASI_EINVAL, + WasiFsError::IOError => __WASI_EIO, + WasiFsError::NotAFile => __WASI_EINVAL, + WasiFsError::NotConnected => __WASI_ENOTCONN, + WasiFsError::EntityNotFound => __WASI_ENOENT, + WasiFsError::PermissionDenied => __WASI_EPERM, + WasiFsError::TimedOut => __WASI_ETIMEDOUT, + WasiFsError::UnexpectedEof => __WASI_EPROTO, + WasiFsError::WouldBlock => __WASI_EAGAIN, + WasiFsError::WriteZero => __WASI_ENOSPC, + WasiFsError::UnknownError(ec) => ec, + } + } } /// This trait relies on your file closing when it goes out of scope via `Drop` @@ -68,20 +142,20 @@ pub trait WasiFile: std::fmt::Debug + Write + Read + Seek { /// Change the size of the file, if the `new_size` is greater than the current size /// the extra bytes will be allocated and zeroed // TODO: stablize this in 0.7.0 by removing default impl - fn set_len(&mut self, _new_size: __wasi_filesize_t) -> Option<()> { + fn set_len(&mut self, _new_size: __wasi_filesize_t) -> Result<(), WasiFsError> { panic!("Default implementation for compatibilty in the 0.6.X releases; this will be removed in 0.7.0. Please implement WasiFile::allocate for your type before then"); } /// Request deletion of the file // TODO: break this out into a WasiPath trait which is dynamically in Kind::File // this change can't be done until before release - fn unlink(&mut self) -> Option<()> { + fn unlink(&mut self) -> Result<(), WasiFsError> { panic!("Default implementation for compatibilty in the 0.6.X releases; this will be removed in 0.7.0. Please implement WasiFile::unlink for your type before then"); } /// Store file contents and metadata to disk // TODO: stablize this in 0.7.0 by removing default impl - fn sync_to_disk(&self) -> Option<()> { + fn sync_to_disk(&self) -> Result<(), WasiFsError> { panic!("Default implementation for compatibilty in the 0.6.X releases; this will be removed in 0.7.0. Please implement WasiFile::sync_to_disk for your type before then"); } } @@ -187,15 +261,42 @@ impl WasiFile for HostFile { self.metadata().len() } - fn set_len(&mut self, new_size: __wasi_filesize_t) -> Option<()> { - fs::File::set_len(&self.inner, new_size).ok() + fn set_len(&mut self, new_size: __wasi_filesize_t) -> Result<(), WasiFsError> { + fs::File::set_len(&self.inner, new_size).map_err(Into::into) } - fn unlink(&mut self) -> Option<()> { - std::fs::remove_file(&self.host_path).ok() + fn unlink(&mut self) -> Result<(), WasiFsError> { + std::fs::remove_file(&self.host_path).map_err(Into::into) + } + fn sync_to_disk(&self) -> Result<(), WasiFsError> { + self.inner.sync_all().map_err(Into::into) } - fn sync_to_disk(&self) -> Option<()> { - self.inner.sync_all().ok() +} + +impl From for WasiFsError { + fn from(io_error: io::Error) -> Self { + match io_error.kind() { + io::ErrorKind::AddrInUse => WasiFsError::AddressInUse, + io::ErrorKind::AddrNotAvailable => WasiFsError::AddressNotAvailable, + io::ErrorKind::AlreadyExists => WasiFsError::AlreadyExists, + io::ErrorKind::BrokenPipe => WasiFsError::BrokenPipe, + io::ErrorKind::ConnectionAborted => WasiFsError::ConnectionAborted, + io::ErrorKind::ConnectionRefused => WasiFsError::ConnectionRefused, + io::ErrorKind::ConnectionReset => WasiFsError::ConnectionReset, + io::ErrorKind::Interrupted => WasiFsError::Interrupted, + io::ErrorKind::InvalidData => WasiFsError::InvalidData, + io::ErrorKind::InvalidInput => WasiFsError::InvalidInput, + io::ErrorKind::NotConnected => WasiFsError::NotConnected, + io::ErrorKind::NotFound => WasiFsError::EntityNotFound, + io::ErrorKind::PermissionDenied => WasiFsError::PermissionDenied, + io::ErrorKind::TimedOut => WasiFsError::TimedOut, + io::ErrorKind::UnexpectedEof => WasiFsError::UnexpectedEof, + io::ErrorKind::WouldBlock => WasiFsError::WouldBlock, + io::ErrorKind::WriteZero => WasiFsError::WriteZero, + io::ErrorKind::Other => WasiFsError::IOError, + // if the following triggers, a new error type was added to this non-exhaustive enum + _ => WasiFsError::UnknownError(__WASI_EIO), + } } } diff --git a/lib/wasi/src/syscalls/mod.rs b/lib/wasi/src/syscalls/mod.rs index 422b67dcee8..5c160623c12 100644 --- a/lib/wasi/src/syscalls/mod.rs +++ b/lib/wasi/src/syscalls/mod.rs @@ -10,7 +10,7 @@ use crate::{ ptr::{Array, WasmPtr}, state::{ self, host_file_type_to_wasi_file_type, Fd, HostFile, Inode, InodeVal, Kind, WasiFile, - WasiState, MAX_SYMLINKS, + WasiFsError, WasiState, MAX_SYMLINKS, }, ExitCode, }; @@ -338,7 +338,7 @@ pub fn fd_allocate( match &mut state.fs.inodes[inode].kind { Kind::File { handle, .. } => { if let Some(handle) = handle { - wasi_try!(handle.set_len(new_size), __WASI_EIO); + wasi_try!(handle.set_len(new_size).map_err(WasiFsError::into_wasi_err)); } else { return __WASI_EBADF; } @@ -549,7 +549,7 @@ pub fn fd_filestat_set_size( match &mut state.fs.inodes[inode].kind { Kind::File { handle, .. } => { if let Some(handle) = handle { - wasi_try!(handle.set_len(st_size), __WASI_EIO); + wasi_try!(handle.set_len(st_size).map_err(WasiFsError::into_wasi_err)); } else { return __WASI_EBADF; } @@ -1159,7 +1159,7 @@ pub fn fd_sync(ctx: &mut Ctx, fd: __wasi_fd_t) -> __wasi_errno_t { match &mut state.fs.inodes[inode].kind { Kind::File { handle, .. } => { if let Some(h) = handle { - wasi_try!(h.sync_to_disk(), __WASI_EIO); + wasi_try!(h.sync_to_disk().map_err(WasiFsError::into_wasi_err)); } else { return __WASI_EINVAL; } @@ -1935,6 +1935,20 @@ pub fn path_rename( debug!("wasi::path_rename"); unimplemented!("wasi::path_rename") } + +/// ### `path_symlink()` +/// Create a symlink +/// Inputs: +/// - `const char *old_path` +/// Array of UTF-8 bytes representing the source path +/// - `u32 old_path_len` +/// The number of bytes to read from `old_path` +/// - `__wasi_fd_t fd` +/// The base directory from which the paths are understood +/// - `const char *new_path` +/// Array of UTF-8 bytes representing the target path +/// - `u32 new_path_len` +/// The number of bytes to read from `new_path` pub fn path_symlink( ctx: &mut Ctx, old_path: WasmPtr, @@ -1944,16 +1958,39 @@ pub fn path_symlink( new_path_len: u32, ) -> __wasi_errno_t { debug!("wasi::path_symlink"); - unimplemented!("wasi::path_symlink") + let state = get_wasi_state(ctx); + let memory = ctx.memory(0); + let old_path_str = wasi_try!( + old_path.get_utf8_string(memory, old_path_len), + __WASI_EINVAL + ); + let new_path_str = wasi_try!( + new_path.get_utf8_string(memory, new_path_len), + __WASI_EINVAL + ); + let base_fd = wasi_try!(state.fs.get_fd(fd)); + if !has_rights(base_fd.rights, __WASI_RIGHT_PATH_SYMLINK) { + return __WASI_EACCES; + } + + __WASI_ESUCCESS } +/// ### `path_unlink_file()` +/// Unlink a file, deleting if the number of hardlinks is 1 +/// Inputs: +/// - `__wasi_fd_t fd` +/// The base file descriptor from which the path is understood +/// - `const char *path` +/// Array of UTF-8 bytes representing the path +/// - `u32 path_len` +/// The number of bytes in the `path` array pub fn path_unlink_file( ctx: &mut Ctx, fd: __wasi_fd_t, path: WasmPtr, path_len: u32, ) -> __wasi_errno_t { - // TODO check if fd is a dir, ensure it's within sandbox, etc. debug!("wasi::path_unlink_file"); let state = get_wasi_state(ctx); let memory = ctx.memory(0); @@ -1992,7 +2029,7 @@ pub fn path_unlink_file( match &mut state.fs.inodes[removed_inode].kind { Kind::File { handle, path } => { if let Some(h) = handle { - wasi_try!(h.unlink().ok_or(__WASI_EIO)); + wasi_try!(h.unlink().map_err(WasiFsError::into_wasi_err)); } else { // File is closed // problem with the abstraction, we can't call unlink because there's no handle @@ -2000,6 +2037,7 @@ pub fn path_unlink_file( wasi_try!(std::fs::remove_file(path).map_err(|_| __WASI_EIO)); } } + Kind::Dir { .. } | Kind::Root { .. } => return __WASI_EISDIR, _ => unimplemented!("wasi::path_unlink_file for non-files"), } let inode_was_removed = unsafe { state.fs.remove_inode(removed_inode) }; From d0696a0c0e577772c1519d833a079cc8cbfe73eb Mon Sep 17 00:00:00 2001 From: Mark McCaskey Date: Thu, 8 Aug 2019 16:42:27 +0900 Subject: [PATCH 2/3] implement wasi::path_symlink test & fix readlink test --- lib/wasi-tests/tests/wasitests/mod.rs | 1 + .../tests/wasitests/path_symlink.rs | 20 ++++ lib/wasi-tests/wasitests/path_symlink.out | 4 + lib/wasi-tests/wasitests/path_symlink.rs | 30 ++++++ lib/wasi-tests/wasitests/path_symlink.wasm | Bin 0 -> 80678 bytes lib/wasi/src/state/mod.rs | 94 +++++++++++++++++- lib/wasi/src/syscalls/mod.rs | 59 ++++++++++- 7 files changed, 204 insertions(+), 4 deletions(-) create mode 100644 lib/wasi-tests/tests/wasitests/path_symlink.rs create mode 100644 lib/wasi-tests/wasitests/path_symlink.out create mode 100644 lib/wasi-tests/wasitests/path_symlink.rs create mode 100755 lib/wasi-tests/wasitests/path_symlink.wasm diff --git a/lib/wasi-tests/tests/wasitests/mod.rs b/lib/wasi-tests/tests/wasitests/mod.rs index a4830aa51f1..c9309aea072 100644 --- a/lib/wasi-tests/tests/wasitests/mod.rs +++ b/lib/wasi-tests/tests/wasitests/mod.rs @@ -16,6 +16,7 @@ mod fseek; mod hello; mod mapdir; mod path_link; +mod path_symlink; mod quine; mod readlink; mod wasi_sees_virtual_root; diff --git a/lib/wasi-tests/tests/wasitests/path_symlink.rs b/lib/wasi-tests/tests/wasitests/path_symlink.rs new file mode 100644 index 00000000000..43f0836bf0a --- /dev/null +++ b/lib/wasi-tests/tests/wasitests/path_symlink.rs @@ -0,0 +1,20 @@ +#[test] +fn test_path_symlink() { + assert_wasi_output!( + "../../wasitests/path_symlink.wasm", + "path_symlink", + vec![], + vec![ + ( + "temp".to_string(), + ::std::path::PathBuf::from("wasitests/test_fs/temp") + ), + ( + "hamlet".to_string(), + ::std::path::PathBuf::from("wasitests/test_fs/hamlet") + ), + ], + vec![], + "../../wasitests/path_symlink.out" + ); +} diff --git a/lib/wasi-tests/wasitests/path_symlink.out b/lib/wasi-tests/wasitests/path_symlink.out new file mode 100644 index 00000000000..085a9281695 --- /dev/null +++ b/lib/wasi-tests/wasitests/path_symlink.out @@ -0,0 +1,4 @@ +ACT III +SCENE I. A room in the castle. + + Enter KING CLAUDIUS, diff --git a/lib/wasi-tests/wasitests/path_symlink.rs b/lib/wasi-tests/wasitests/path_symlink.rs new file mode 100644 index 00000000000..2c5a71c8550 --- /dev/null +++ b/lib/wasi-tests/wasitests/path_symlink.rs @@ -0,0 +1,30 @@ +// Args: +// mapdir: temp:wasitests/test_fs/temp +// mapdir: hamlet:wasitests/test_fs/hamlet + +use std::fs; +use std::io::{Read, Seek, SeekFrom}; +use std::path::PathBuf; + +fn main() { + #[cfg(not(target_os = "wasi"))] + let mut base = PathBuf::from("wasitests/test_fs"); + #[cfg(target_os = "wasi")] + let mut base = PathBuf::from("/"); + + let symlink_loc = base.join("temp/act3"); + let symlink_target = "../hamlet/act3"; + let scene1 = symlink_loc.join("scene1.txt"); + + std::fs::soft_link(&symlink_target, &symlink_loc); + + let mut file = fs::File::open(&scene1).expect("Could not open file"); + + let mut buffer = [0u8; 64]; + + assert_eq!(file.read(&mut buffer).unwrap(), 64); + let str_val = std::str::from_utf8(&buffer[..]).unwrap(); + println!("{}", str_val); + + std::fs::remove_file(symlink_loc).unwrap(); +} diff --git a/lib/wasi-tests/wasitests/path_symlink.wasm b/lib/wasi-tests/wasitests/path_symlink.wasm new file mode 100755 index 0000000000000000000000000000000000000000..3a773eaedd8eab6963a75bf44b705edd965f334d GIT binary patch literal 80678 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 literal 0 HcmV?d00001 diff --git a/lib/wasi/src/state/mod.rs b/lib/wasi/src/state/mod.rs index b74dc927c9a..fb1c4f63ac8 100644 --- a/lib/wasi/src/state/mod.rs +++ b/lib/wasi/src/state/mod.rs @@ -393,8 +393,11 @@ impl WasiFs { let path: &Path = Path::new(path); let mut cur_inode = base_dir.inode; + let n_components = path.components().count(); // TODO: rights checks - 'path_iter: for component in path.components() { + 'path_iter: for (i, component) in path.components().enumerate() { + // used to terminate symlink resolution properly + let last_component = i + 1 == n_components; // for each component traverse file structure // loading inodes as necessary 'symlink_resolution: while symlink_count < MAX_SYMLINKS { @@ -430,7 +433,6 @@ impl WasiFs { cd.push(component); cd }; - // TODO: verify this returns successfully when given a non-symlink let metadata = file.symlink_metadata().ok().ok_or(__WASI_EINVAL)?; let file_type = metadata.file_type(); // we want to insert newly opened dirs and files, but not transient symlinks @@ -488,6 +490,7 @@ impl WasiFs { cur_inode = new_inode; if loop_for_symlink && follow_symlinks { + debug!("Following symlink to {:?}", cur_inode); continue 'symlink_resolution; } } @@ -530,6 +533,7 @@ impl WasiFs { base.push(relative_path); base.to_string_lossy().to_string() }; + debug!("Following symlink recursively"); let symlink_inode = self.get_inode_at_path_inner( new_base_dir, &new_path, @@ -537,7 +541,15 @@ impl WasiFs { follow_symlinks, )?; cur_inode = symlink_inode; - //continue 'symlink_resolution; + // if we're at the very end and we found a file, then we're done + // TODO: figure out if this should also happen for directories? + if let Kind::File { .. } = &self.inodes[cur_inode].kind { + // check if on last step + if last_component { + break 'symlink_resolution; + } + } + continue 'symlink_resolution; } } break 'symlink_resolution; @@ -571,6 +583,64 @@ impl WasiFs { Err(__WASI_EINVAL) // this may not make sense } + // if this is still dead code and the year is 2020 or later, please delete this function + #[allow(dead_code)] + pub(crate) fn path_relative_to_fd( + &self, + fd: __wasi_fd_t, + inode: Inode, + ) -> Result { + let mut stack = vec![]; + let base_fd = self.get_fd(fd)?; + let base_inode = base_fd.inode; + let mut cur_inode = inode; + + while cur_inode != base_inode { + stack.push(self.inodes[cur_inode].name.clone()); + match &self.inodes[cur_inode].kind { + Kind::Dir { parent, .. } => { + if let Some(p) = parent { + cur_inode = *p; + } + } + _ => return Err(__WASI_EINVAL), + } + } + + let mut out = PathBuf::new(); + for p in stack.iter().rev() { + out.push(p); + } + Ok(out) + } + + /// finds the number of directories between the fd and the inode if they're connected + /// expects inode to point to a directory + pub(crate) fn path_depth_from_fd( + &self, + fd: __wasi_fd_t, + inode: Inode, + ) -> Result { + let mut counter = 0; + let base_fd = self.get_fd(fd)?; + let base_inode = base_fd.inode; + let mut cur_inode = inode; + + while cur_inode != base_inode { + counter += 1; + match &self.inodes[cur_inode].kind { + Kind::Dir { parent, .. } => { + if let Some(p) = parent { + cur_inode = *p; + } + } + _ => return Err(__WASI_EINVAL), + } + } + + Ok(counter) + } + /// gets a host file from a base directory and a path /// this function ensures the fs remains sandboxed // NOTE: follow symlinks is super weird right now @@ -704,6 +774,24 @@ impl WasiFs { })) } + /// creates an inode and inserts it given a Kind, does not assume the file exists to + pub fn create_inode_with_default_stat( + &mut self, + kind: Kind, + is_preopened: bool, + name: String, + ) -> Inode { + let mut stat = __wasi_filestat_t::default(); + stat.st_ino = self.get_next_inode_index(); + + self.inodes.insert(InodeVal { + stat, + is_preopened, + name, + kind, + }) + } + pub fn create_fd( &mut self, rights: __wasi_rights_t, diff --git a/lib/wasi/src/syscalls/mod.rs b/lib/wasi/src/syscalls/mod.rs index 5c160623c12..0000cb6d6b3 100644 --- a/lib/wasi/src/syscalls/mod.rs +++ b/lib/wasi/src/syscalls/mod.rs @@ -1679,6 +1679,10 @@ pub fn path_open( dirflags & __WASI_LOOKUP_SYMLINK_FOLLOW != 0, ); + if let Ok(m) = maybe_inode { + dbg!(&state.fs.inodes[m]); + } + // TODO: traverse rights of dirs properly // COMMENTED OUT: WASI isn't giving appropriate rights here when opening // TODO: look into this; file a bug report if this is a bug @@ -1973,6 +1977,56 @@ pub fn path_symlink( return __WASI_EACCES; } + // get the depth of the parent + 1 (UNDER INVESTIGATION HMMMMMMMM THINK FISH ^ THINK FISH) + let old_path_path = std::path::Path::new(old_path_str); + let (source_inode, _) = wasi_try!(state.fs.get_parent_inode_at_path(fd, old_path_path, true)); + let depth = wasi_try!(state.fs.path_depth_from_fd(fd, source_inode)) - 1; + + let new_path_path = std::path::Path::new(new_path_str); + let (target_parent_inode, entry_name) = + wasi_try!(state.fs.get_parent_inode_at_path(fd, new_path_path, true)); + + // short circuit if anything is wrong, before we create an inode + match &state.fs.inodes[target_parent_inode].kind { + Kind::Dir { entries, .. } => { + if entries.contains_key(&entry_name) { + return __WASI_EEXIST; + } + } + Kind::Root { .. } => return __WASI_ENOTCAPABLE, + Kind::File { .. } | Kind::Symlink { .. } | Kind::Buffer { .. } => { + unreachable!("get_parent_inode_at_path returned something other than a Dir or Root") + } + } + + let mut source_path = std::path::Path::new(old_path_str); + let mut relative_path = std::path::PathBuf::new(); + for _ in 0..depth { + relative_path.push(".."); + } + relative_path.push(source_path); + debug!( + "Symlinking {} to {}", + new_path_str, + relative_path.to_string_lossy() + ); + + let kind = Kind::Symlink { + base_po_dir: fd, + path_to_symlink: std::path::PathBuf::from(new_path_str), + relative_path, + }; + let new_inode = state + .fs + .create_inode_with_default_stat(kind, false, entry_name.clone()); + + if let Kind::Dir { + ref mut entries, .. + } = &mut state.fs.inodes[target_parent_inode].kind + { + entries.insert(entry_name, new_inode); + } + __WASI_ESUCCESS } @@ -2038,7 +2092,10 @@ pub fn path_unlink_file( } } Kind::Dir { .. } | Kind::Root { .. } => return __WASI_EISDIR, - _ => unimplemented!("wasi::path_unlink_file for non-files"), + Kind::Symlink { .. } => { + // TODO: actually delete real symlinks and do nothing for virtual symlinks + } + _ => unimplemented!("wasi::path_unlink_file for Buffer"), } let inode_was_removed = unsafe { state.fs.remove_inode(removed_inode) }; assert!( From e29a89a3f81f3a4ddff27c06fab11f495f90bdb5 Mon Sep 17 00:00:00 2001 From: Mark McCaskey Date: Fri, 9 Aug 2019 09:32:38 +0900 Subject: [PATCH 3/3] add path_symlink entry to changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b490f36e44..d93da9b6836 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Blocks of changes will separated by version increments. Special thanks to @YaronWittenstein @penberg for their contributions. +- [#643](https://github.com/wasmerio/wasmer/issues/643) Implement `wasi::path_symlink` and improve WASI FS public api IO error reporting - [#608](https://github.com/wasmerio/wasmer/issues/608) Implement wasi syscalls `fd_allocate`, `fd_sync`, `fd_pread`, `path_link`, `path_filestat_set_times`; update WASI fs API in a WIP way; reduce coupling of WASI code to host filesystem; make debug messages from WASI more readable; improve rights-checking when calling syscalls; implement reference counting on inodes; misc bug fixes and improvements - [#616](https://github.com/wasmerio/wasmer/issues/616) Create the import object separately from instance instantiation in `runtime-c-api` - [#620](https://github.com/wasmerio/wasmer/issues/620) Replace one `throw()` with `noexcept` in llvm backend