diff --git a/crates/test-programs/build.rs b/crates/test-programs/build.rs index 44b62b5deff6..aab9d66d2d58 100644 --- a/crates/test-programs/build.rs +++ b/crates/test-programs/build.rs @@ -233,7 +233,6 @@ mod wasi_tests { "dangling_symlink" => true, "symlink_loop" => true, "truncation_rights" => true, - "path_link" => true, "dangling_fd" => true, // TODO: virtfs files cannot be poll_oneoff'd yet "poll_oneoff_virtualfs" => true, diff --git a/crates/test-programs/wasi-tests/src/bin/path_link.rs b/crates/test-programs/wasi-tests/src/bin/path_link.rs index eff44662fd65..c459d7f79446 100644 --- a/crates/test-programs/wasi-tests/src/bin/path_link.rs +++ b/crates/test-programs/wasi-tests/src/bin/path_link.rs @@ -74,6 +74,8 @@ unsafe fn check_rights(orig_fd: wasi::Fd, link_fd: wasi::Fd) { fdstats_assert_eq(orig_fdstat, link_fdstat); } +// Extra calls of fd_close are needed for Windows, which will not remove +// the directory until all handles are closed. unsafe fn test_path_link(dir_fd: wasi::Fd) { // Create a file let file_fd = create_or_open(dir_fd, "file", wasi::OFLAGS_CREAT); @@ -81,17 +83,20 @@ unsafe fn test_path_link(dir_fd: wasi::Fd) { // Create a link in the same directory and compare rights wasi::path_link(dir_fd, 0, "file", dir_fd, "link") .expect("creating a link in the same directory"); - let mut link_fd = open_link(dir_fd, "link"); + let link_fd = open_link(dir_fd, "link"); check_rights(file_fd, link_fd); + wasi::fd_close(link_fd).expect("Closing link_fd"); // needed for Windows wasi::path_unlink_file(dir_fd, "link").expect("removing a link"); // Create a link in a different directory and compare rights wasi::path_create_directory(dir_fd, "subdir").expect("creating a subdirectory"); let subdir_fd = create_or_open(dir_fd, "subdir", wasi::OFLAGS_DIRECTORY); wasi::path_link(dir_fd, 0, "file", subdir_fd, "link").expect("creating a link in subdirectory"); - link_fd = open_link(subdir_fd, "link"); + let link_fd = open_link(subdir_fd, "link"); check_rights(file_fd, link_fd); wasi::path_unlink_file(subdir_fd, "link").expect("removing a link"); + wasi::fd_close(subdir_fd).expect("Closing subdir_fd"); // needed for Windows + wasi::fd_close(link_fd).expect("Closing link_fd"); // needed for Windows wasi::path_remove_directory(dir_fd, "subdir").expect("removing a subdirectory"); // Create a link to a path that already exists @@ -188,7 +193,7 @@ unsafe fn test_path_link(dir_fd: wasi::Fd) { "link", ) .expect("creating a link to a file following symlinks"); - link_fd = open_link(dir_fd, "link"); + let link_fd = open_link(dir_fd, "link"); check_rights(file_fd, link_fd); wasi::path_unlink_file(dir_fd, "link").expect("removing a link"); wasi::path_unlink_file(dir_fd, "symlink").expect("removing a symlink"); diff --git a/crates/wasi-common/src/sys/windows/mod.rs b/crates/wasi-common/src/sys/windows/mod.rs index bb559be5e2b8..b875f4b1c3d6 100644 --- a/crates/wasi-common/src/sys/windows/mod.rs +++ b/crates/wasi-common/src/sys/windows/mod.rs @@ -69,7 +69,7 @@ impl From for Errno { winerror::ERROR_DIRECTORY => Self::Notdir, winerror::ERROR_ALREADY_EXISTS => Self::Exist, x => { - log::debug!("unknown error value: {}", x); + log::debug!("winerror: unknown error value: {}", x); Self::Io } }, diff --git a/crates/wasi-common/src/sys/windows/path.rs b/crates/wasi-common/src/sys/windows/path.rs index 32f9d7e46d49..bf101cd01e1d 100644 --- a/crates/wasi-common/src/sys/windows/path.rs +++ b/crates/wasi-common/src/sys/windows/path.rs @@ -3,6 +3,7 @@ use crate::fd; use crate::path::PathGet; use crate::sys::entry::OsHandle; use crate::wasi::{types, Errno, Result}; +use log::debug; use std::convert::TryInto; use std::ffi::{OsStr, OsString}; use std::fs::{File, OpenOptions}; @@ -165,11 +166,48 @@ pub(crate) fn create_directory(file: &File, path: &str) -> Result<()> { } pub(crate) fn link( - _resolved_old: PathGet, - _resolved_new: PathGet, - _follow_symlinks: bool, + resolved_old: PathGet, + resolved_new: PathGet, + follow_symlinks: bool, ) -> Result<()> { - unimplemented!("path_link") + use std::fs; + let mut old_path = resolved_old.concatenate()?; + let new_path = resolved_new.concatenate()?; + if follow_symlinks { + // in particular, this will return an error if the target path doesn't exist + debug!("Following symlinks for path: {:?}", old_path); + old_path = fs::canonicalize(&old_path).map_err(|e| match e.raw_os_error() { + // fs::canonicalize under Windows will return: + // * ERROR_FILE_NOT_FOUND, if it encounters a dangling symlink + // * ERROR_CANT_RESOLVE_FILENAME, if it encounters a symlink loop + Some(code) if code as u32 == winerror::ERROR_CANT_RESOLVE_FILENAME => Errno::Loop, + _ => e.into(), + })?; + } + fs::hard_link(&old_path, &new_path).or_else(|err| { + match err.raw_os_error() { + Some(code) => { + debug!("path_link at fs::hard_link error code={:?}", code); + match code as u32 { + winerror::ERROR_ACCESS_DENIED => { + // If an attempt is made to create a hard link to a directory, POSIX-compliant + // implementations of link return `EPERM`, but `ERROR_ACCESS_DENIED` is converted + // to `EACCES`. We detect and correct this case here. + if fs::metadata(&old_path).map(|m| m.is_dir()).unwrap_or(false) { + return Err(Errno::Perm); + } + } + _ => {} + } + + Err(err.into()) + } + None => { + log::debug!("Inconvertible OS error: {}", err); + Err(Errno::Io) + } + } + }) } pub(crate) fn open(