diff --git a/.github/workflows/wasi.yml b/.github/workflows/wasi.yml index 8c3721d6ac5..00bb9f65988 100644 --- a/.github/workflows/wasi.yml +++ b/.github/workflows/wasi.yml @@ -50,7 +50,7 @@ jobs: # Tests incompatible with WASI are annotated with # #[cfg_attr(wasi_runner, ignore)] in the test source files. # TODO: add integration tests for these tools as WASI support is extended: - # arch b2sum cat cksum cp csplit date dir dircolors fmt join ln + # arch b2sum cat cksum cp csplit date dir dircolors fmt join # ls md5sum mkdir mv nproc pathchk pr printenv ptx pwd readlink # realpath rm rmdir seq sha1sum sha224sum sha256sum sha384sum # sha512sum shred sleep sort split tail touch tsort uname uniq @@ -61,7 +61,7 @@ jobs: test_base32:: test_base64:: test_basenc:: test_basename:: \ test_comm:: test_cut:: test_dirname:: test_echo:: \ test_expand:: test_factor:: test_false:: test_fold:: \ - test_head:: test_link:: test_nl:: test_numfmt:: \ + test_head:: test_link:: test_ln:: test_nl:: test_numfmt:: \ test_od:: test_paste:: test_printf:: test_shuf:: test_sum:: \ test_tee:: test_tr:: test_true:: test_truncate:: \ test_unexpand:: test_unlink:: test_wc:: diff --git a/Cargo.lock b/Cargo.lock index d181f4d914e..d1cc9546e76 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3728,6 +3728,7 @@ version = "0.8.0" dependencies = [ "clap", "fluent", + "rustix", "thiserror 2.0.18", "uucore", ] diff --git a/docs/src/wasi-test-gaps.md b/docs/src/wasi-test-gaps.md index 18406243c9b..4789ef03dc9 100644 --- a/docs/src/wasi-test-gaps.md +++ b/docs/src/wasi-test-gaps.md @@ -6,7 +6,7 @@ To find all annotated tests: `grep -rn 'wasi_runner, ignore' tests/` ## Tools not yet covered by integration tests -arch, b2sum, cat, cksum, cp, csplit, date, dir, dircolors, fmt, join, ln, ls, md5sum, mkdir, mv, nproc, pathchk, pr, printenv, ptx, pwd, readlink, realpath, rm, rmdir, seq, sha1sum, sha224sum, sha256sum, sha384sum, sha512sum, shred, sleep, sort, split, tail, touch, tsort, uname, uniq, vdir, yes +arch, b2sum, cat, cksum, cp, csplit, date, dir, dircolors, fmt, join, ls, md5sum, mkdir, mv, nproc, pathchk, pr, printenv, ptx, pwd, readlink, realpath, rm, rmdir, seq, sha1sum, sha224sum, sha256sum, sha384sum, sha512sum, shred, sleep, sort, split, tail, touch, tsort, uname, uniq, vdir, yes ## WASI sandbox: host paths not visible @@ -31,3 +31,7 @@ WASI does not support spawning child processes. Tests that shell out to other co ## WASI: stdin file position not preserved through wasmtime When stdin is a seekable file, wasmtime does not preserve the file position between the host and guest. Tests that validate stdin offset behavior after `head` reads are skipped. + +## WASI: read_link on absolute paths fails under wasmtime via spawned test harness + +`fs::read_link` on an absolute path inside the sandbox (e.g. `/file2`) returns `EPERM` when the WASI binary is launched through `std::process::Command` from the test harness, even though the same call works when wasmtime is invoked directly. This breaks `uucore::fs::canonicalize` for symlink sources, so tests that rely on following a symlink to compute a relative path are skipped. diff --git a/src/uu/ln/Cargo.toml b/src/uu/ln/Cargo.toml index 6527ee89a2b..6b4401e3a9d 100644 --- a/src/uu/ln/Cargo.toml +++ b/src/uu/ln/Cargo.toml @@ -20,6 +20,7 @@ path = "src/ln.rs" [dependencies] clap = { workspace = true } +rustix = { workspace = true, features = ["fs"] } uucore = { workspace = true, features = ["backup-control", "fs"] } thiserror = { workspace = true } fluent = { workspace = true } diff --git a/src/uu/ln/src/ln.rs b/src/uu/ln/src/ln.rs index 5fb75a86730..e67bd6f42f3 100644 --- a/src/uu/ln/src/ln.rs +++ b/src/uu/ln/src/ln.rs @@ -19,6 +19,8 @@ use std::ffi::OsString; use std::fs; use thiserror::Error; +#[cfg(target_os = "wasi")] +use std::io; #[cfg(any(unix, target_os = "redox"))] use std::os::unix::fs::symlink; #[cfg(windows)] @@ -490,9 +492,6 @@ pub fn symlink, P2: AsRef>(src: P1, dst: P2) -> std::io::R } #[cfg(target_os = "wasi")] -fn symlink, P2: AsRef>(_src: P1, _dst: P2) -> std::io::Result<()> { - Err(std::io::Error::new( - std::io::ErrorKind::Unsupported, - "symlinks not supported on this platform", - )) +pub fn symlink, P2: AsRef>(src: P1, dst: P2) -> io::Result<()> { + rustix::fs::symlink(src.as_ref(), dst.as_ref()).map_err(io::Error::from) } diff --git a/src/uucore/src/lib/features/backup_control.rs b/src/uucore/src/lib/features/backup_control.rs index f56d131c7d6..34f73521061 100644 --- a/src/uucore/src/lib/features/backup_control.rs +++ b/src/uucore/src/lib/features/backup_control.rs @@ -91,6 +91,7 @@ use std::{ error::Error, ffi::{OsStr, OsString}, fmt::{Debug, Display}, + fs, path::{Path, PathBuf}, }; @@ -442,7 +443,11 @@ fn numbered_backup_path(path: &Path) -> PathBuf { let mut i: u64 = 1; loop { let new_path = simple_backup_path(path, OsString::from(format!(".~{i}~"))); - if !new_path.exists() { + // Use `symlink_metadata` rather than `exists()` so that a dangling + // symlink still counts as an existing backup (avoiding a silent + // overwrite), and so we do not report a live symlink as missing when + // the target cannot be stat'd. + if fs::symlink_metadata(&new_path).is_err() { return new_path; } i += 1; @@ -451,7 +456,7 @@ fn numbered_backup_path(path: &Path) -> PathBuf { fn existing_backup_path>(path: &Path, suffix: S) -> PathBuf { let test_path = simple_backup_path(path, OsString::from(".~1~")); - if test_path.exists() { + if fs::symlink_metadata(&test_path).is_ok() { return numbered_backup_path(path); } simple_backup_path(path, suffix.as_ref()) diff --git a/tests/by-util/test_ln.rs b/tests/by-util/test_ln.rs index 560a7364f2f..9d8bfdf995b 100644 --- a/tests/by-util/test_ln.rs +++ b/tests/by-util/test_ln.rs @@ -501,6 +501,7 @@ fn test_symlink_implicit_target_dir() { } #[test] +#[cfg_attr(wasi_runner, ignore = "WASI sandbox: host paths not visible")] fn test_symlink_to_dir_2args() { let (at, mut ucmd) = at_and_ucmd!(); let filename = "test_symlink_to_dir_2args_file"; @@ -754,6 +755,10 @@ fn test_relative_dst_already_symlink() { } #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI: read_link on absolute paths fails under wasmtime via spawned test harness" +)] fn test_relative_src_already_symlink() { let (at, mut ucmd) = at_and_ucmd!(); at.touch("file1"); @@ -967,6 +972,7 @@ fn test_ln_seen_file() { #[test] #[cfg(target_os = "linux")] +#[cfg_attr(wasi_runner, ignore = "WASI: argv/filenames must be valid UTF-8")] fn test_ln_non_utf8_paths() { use std::ffi::OsStr; use std::os::unix::ffi::OsStrExt;