From c0a4356949680a4531e173fd23e96d47eb5efccd Mon Sep 17 00:00:00 2001 From: Eduardo Rodrigues <16357187+eduardomourar@users.noreply.github.com> Date: Sat, 1 Aug 2026 03:15:12 +0100 Subject: [PATCH] fix(wasi): cp attribute preservation and symlink handling cp treats ENOSYS like EOPNOTSUPP when skipping optional attribute preservation (WASI has no chmod/chown at all), reads timestamps via Metadata::accessed/modified instead of the filetime crate (which panics there), creates symlinks via rustix::fs::symlink, and only errors out of --reflink when explicitly requested rather than whenever it isn't Never. --- src/uu/cp/Cargo.toml | 2 +- src/uu/cp/src/cp.rs | 85 +++++++++++++++++++++++++-------- src/uu/cp/src/platform/other.rs | 2 +- 3 files changed, 67 insertions(+), 22 deletions(-) diff --git a/src/uu/cp/Cargo.toml b/src/uu/cp/Cargo.toml index cf412e89be6..0b33f9ea9bc 100644 --- a/src/uu/cp/Cargo.toml +++ b/src/uu/cp/Cargo.toml @@ -38,7 +38,7 @@ walkdir = { workspace = true } indicatif = { workspace = true } thiserror = { workspace = true } fluent = { workspace = true } -rustix = { workspace = true } +rustix = { workspace = true, features = ["fs"] } [target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies] selinux = { workspace = true, optional = true } diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index 75f79816a5a..3eef3948b64 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -1374,7 +1374,18 @@ fn is_enotsup_error(error: &CpError) -> bool { const EOPNOTSUPP: i32 = 95; match error { - CpError::IoErr(e) | CpError::IoErrContext(e, _) => e.raw_os_error() == Some(EOPNOTSUPP), + CpError::IoErr(e) | CpError::IoErrContext(e, _) => { + let raw = e.raw_os_error(); + // WASI's sandbox has no chmod/chown syscalls at all (not merely an + // unsupported combination of flags), so `fs::set_permissions` and + // friends always fail with ENOSYS there. Treat that the same as + // EOPNOTSUPP for optional preservation. + #[cfg(target_os = "wasi")] + if raw == Some(libc::ENOSYS) { + return true; + } + raw == Some(EOPNOTSUPP) + } _ => false, } } @@ -1709,6 +1720,40 @@ impl OverwriteMode { /// Note: ENOTSUP/EOPNOTSUPP errors are silently ignored when not required, as per GNU cp /// documentation: "Try to preserve SELinux security context and extended attributes (xattr), /// but ignore any failure to do that and print no corresponding diagnostic." +/// Returns the source's last access and modification times. +/// +/// `filetime::FileTime::from_last_{access,modification}_time` panics on +/// WASI (the `filetime` crate has no WASI-specific backend and falls back +/// to its unimplemented generic wasm one). `Metadata::accessed`/`modified` +/// are stable and WASI-backed, so use those instead there. +// On non-wasi targets this can never fail, but the wasi branch below can. +#[cfg_attr(not(target_os = "wasi"), allow(clippy::unnecessary_wraps))] +fn source_times(source_metadata: &Metadata, context: &str) -> CopyResult<(FileTime, FileTime)> { + #[cfg(target_os = "wasi")] + { + Ok(( + FileTime::from( + source_metadata + .accessed() + .map_err(|e| CpError::IoErrContext(e, context.to_owned()))?, + ), + FileTime::from( + source_metadata + .modified() + .map_err(|e| CpError::IoErrContext(e, context.to_owned()))?, + ), + )) + } + #[cfg(not(target_os = "wasi"))] + { + let _ = context; + Ok(( + FileTime::from_last_access_time(source_metadata), + FileTime::from_last_modification_time(source_metadata), + )) + } +} + fn handle_preserve CopyResult<()>>(p: Preserve, f: F) -> CopyResult<()> { match p { Preserve::No { .. } => {} @@ -1903,8 +1948,7 @@ pub(crate) fn copy_attributes( })?; handle_preserve(attributes.timestamps, || -> CopyResult<()> { - let atime = FileTime::from_last_access_time(&source_metadata); - let mtime = FileTime::from_last_modification_time(&source_metadata); + let (atime, mtime) = source_times(&source_metadata, context)?; // `set_file_times` opens the destination (O_RDONLY) before calling // futimens; opening a FIFO or device with no peer blocks forever, and a // socket cannot be opened at all. For symlinks and these special files @@ -1973,18 +2017,8 @@ pub(crate) fn copy_attributes( fn symlink_file( source: &Path, dest: &Path, - #[cfg(not(target_os = "wasi"))] symlinked_files: &mut HashSet, - #[cfg(target_os = "wasi")] _symlinked_files: &mut HashSet, + symlinked_files: &mut HashSet, ) -> CopyResult<()> { - #[cfg(target_os = "wasi")] - { - Err(CpError::IoErrContext( - io::Error::new(io::ErrorKind::Unsupported, "symlinks not supported"), - translate!("cp-error-cannot-create-symlink", - "dest" => get_filename(dest).unwrap_or("?").quote(), - "source" => get_filename(source).unwrap_or("?").quote()), - )) - } #[cfg(not(any(windows, target_os = "wasi")))] { std::os::unix::fs::symlink(source, dest).map_err(|e| { @@ -1996,6 +2030,20 @@ fn symlink_file( ) })?; } + // `std::os::unix::fs::symlink` is unavailable on WASI (`std::os::wasi` is + // nightly-only), but `rustix::fs::symlink` works on stable for both + // wasip1 and wasip2 (same approach `ln` already uses). + #[cfg(target_os = "wasi")] + { + rustix::fs::symlink(source, dest).map_err(|e| { + CpError::IoErrContext( + io::Error::from(e), + translate!("cp-error-cannot-create-symlink", + "dest" => get_filename(dest).unwrap_or("?").quote(), + "source" => get_filename(source).unwrap_or("?").quote()), + ) + })?; + } #[cfg(windows)] { std::os::windows::fs::symlink_file(source, dest).map_err(|e| { @@ -2007,13 +2055,10 @@ fn symlink_file( ) })?; } - #[cfg(not(target_os = "wasi"))] - { - if let Ok(file_info) = FileInformation::from_path(dest, false) { - symlinked_files.insert(file_info); - } - Ok(()) + if let Ok(file_info) = FileInformation::from_path(dest, false) { + symlinked_files.insert(file_info); } + Ok(()) } fn context_for(src: &Path, dest: &Path) -> String { diff --git a/src/uu/cp/src/platform/other.rs b/src/uu/cp/src/platform/other.rs index 143c35e185b..8b9bbfbe206 100644 --- a/src/uu/cp/src/platform/other.rs +++ b/src/uu/cp/src/platform/other.rs @@ -19,7 +19,7 @@ pub(crate) fn copy_on_write( sparse_mode: SparseMode, context: &str, ) -> CopyResult { - if reflink_mode != ReflinkMode::Never { + if reflink_mode == ReflinkMode::Always { return Err(translate!("cp-error-reflink-not-supported") .to_string() .into());