diff --git a/src/uu/touch/Cargo.toml b/src/uu/touch/Cargo.toml index 447f795d1bf..ac3f77fde9d 100644 --- a/src/uu/touch/Cargo.toml +++ b/src/uu/touch/Cargo.toml @@ -35,6 +35,10 @@ tempfile = { workspace = true } libc = { workspace = true } rustix = { workspace = true, features = ["fs"] } +[target.'cfg(target_os = "wasi")'.dependencies] +libc = { workspace = true } +rustix = { workspace = true, features = ["fs"] } + [target.'cfg(target_os = "windows")'.dependencies] windows-sys = { workspace = true, features = [ "Win32_Storage_FileSystem", diff --git a/src/uu/touch/src/touch.rs b/src/uu/touch/src/touch.rs index fc9fcc57737..fa4c92efd49 100644 --- a/src/uu/touch/src/touch.rs +++ b/src/uu/touch/src/touch.rs @@ -10,16 +10,18 @@ pub mod error; use clap::builder::{PossibleValue, ValueParser}; use clap::{Arg, ArgAction, ArgGroup, ArgMatches, Command}; -#[cfg(any(not(unix), target_os = "redox"))] +use filetime::FileTime; +#[cfg(any(not(any(unix, target_os = "wasi")), target_os = "redox"))] use filetime::set_file_times; -use filetime::{FileTime, set_symlink_file_times}; +#[cfg(not(target_os = "wasi"))] +use filetime::set_symlink_file_times; use jiff::civil::Time; use jiff::fmt::strtime; use jiff::tz::TimeZone; use jiff::{Timestamp, ToSpan, Zoned}; #[cfg(unix)] use libc::O_NONBLOCK; -#[cfg(unix)] +#[cfg(any(unix, target_os = "wasi"))] use rustix::fs::Timestamps; #[cfg(unix)] use rustix::fs::futimens; @@ -594,11 +596,8 @@ fn update_times( // sets the file access and modification times for a file or a symbolic link. // The filename, access time (atime), and modification time (mtime) are provided as inputs. - if opts.no_deref && !is_stdout { - return set_symlink_file_times(path, atime, mtime).map_err_context( - || translate!("touch-error-setting-times-of-path", "path" => path.quote()), - ); + return set_symlink_times(path, atime, mtime); } #[cfg(unix)] @@ -622,22 +621,46 @@ fn update_times( return Ok(()); } // The write-FD approach fails on special files such as FIFOs (the - // write-only open returns ENXIO when there is no reader). Set the times - // by path with utimensat, which never opens the file and so never + // write-only open returns ENXIO when there is no reader). Fall through + // to set_times_by_path below, which never opens the file and so never // blocks — unlike filetime::set_file_times, which opens O_RDONLY and // would hang on a reader-less FIFO. - set_times_by_path(path, atime, mtime) } - #[cfg(not(unix))] - { - set_file_times(path, atime, mtime).map_err_context( - || translate!("touch-error-setting-times-of-path", "path" => path.quote()), - ) - } + // WASI shares the unix fallback: utimensat never opens the file either, + // and the crate's `set_file_times` isn't available there (see below). + #[cfg(any(unix, target_os = "wasi"))] + return set_times_by_path(path, atime, mtime); + + #[cfg(not(any(unix, target_os = "wasi")))] + set_file_times(path, atime, mtime) + .map_err_context(|| translate!("touch-error-setting-times-of-path", "path" => path.quote())) } -#[cfg(unix)] +/// Set access and modification times on `path` without following symlinks. +#[cfg(not(target_os = "wasi"))] +fn set_symlink_times(path: &Path, atime: FileTime, mtime: FileTime) -> UResult<()> { + set_symlink_file_times(path, atime, mtime) + .map_err_context(|| translate!("touch-error-setting-times-of-path", "path" => path.quote())) +} + +/// `filetime::set_symlink_file_times` always fails on WASI (the crate has no +/// WASI-specific backend), but `rustix::fs::utimensat` with +/// `AT_SYMLINK_NOFOLLOW` works there on stable. +#[cfg(target_os = "wasi")] +fn set_symlink_times(path: &Path, atime: FileTime, mtime: FileTime) -> UResult<()> { + let timestamps = build_timestamps(atime, mtime); + rustix::fs::utimensat( + rustix::fs::CWD, + path, + ×tamps, + rustix::fs::AtFlags::SYMLINK_NOFOLLOW, + ) + .map_err(Error::from) + .map_err_context(|| translate!("touch-error-setting-times-of-path", "path" => path.quote())) +} + +#[cfg(any(unix, target_os = "wasi"))] /// Build a rustix `Timestamps` from the access and modification `FileTime`s, /// preserving the `UTIME_NOW`/`UTIME_OMIT` sentinels in the nanoseconds field. fn build_timestamps(atime: FileTime, mtime: FileTime) -> Timestamps { @@ -653,11 +676,12 @@ fn build_timestamps(atime: FileTime, mtime: FileTime) -> Timestamps { } } -#[cfg(all(unix, not(target_os = "redox")))] -/// Set file times by path using `utimensat`, following symlinks. +#[cfg(all(any(unix, target_os = "wasi"), not(target_os = "redox")))] +/// Set file times by path using `utimensat`. /// -/// This never opens the file, so it does not block on special files such as -/// FIFOs. +/// This never opens the file on Unix, avoiding blocks on FIFOs. On WASI, if +/// `utimensat` fails on a non-symlink (e.g. unopenable mode 0 files), it retries +/// with `SYMLINK_NOFOLLOW`. fn set_times_by_path(path: &Path, atime: FileTime, mtime: FileTime) -> UResult<()> { let timestamps = build_timestamps(atime, mtime); rustix::fs::utimensat( @@ -666,6 +690,18 @@ fn set_times_by_path(path: &Path, atime: FileTime, mtime: FileTime) -> UResult<( ×tamps, rustix::fs::AtFlags::empty(), ) + .or_else(|err| { + if cfg!(target_os = "wasi") && !path.is_symlink() { + rustix::fs::utimensat( + rustix::fs::CWD, + path, + ×tamps, + rustix::fs::AtFlags::SYMLINK_NOFOLLOW, + ) + } else { + Err(err) + } + }) .map_err(|e| Error::from_raw_os_error(e.raw_os_error())) .map_err_context(|| translate!("touch-error-setting-times-of-path", "path" => path.quote())) } @@ -725,9 +761,32 @@ fn stat(path: &Path, follow: bool) -> std::io::Result<(FileTime, FileTime)> { fs::symlink_metadata(path)? }; + atime_mtime_of(&metadata) +} + +/// Extract the access and modification `FileTime`s from `metadata`. +/// +/// Returns a `Result` (rather than the pair directly) to share a signature +/// with the WASI variant below, whose `Metadata::accessed`/`modified` calls +/// can fail. +#[cfg(not(target_os = "wasi"))] +#[expect(clippy::unnecessary_wraps)] +fn atime_mtime_of(metadata: &fs::Metadata) -> std::io::Result<(FileTime, FileTime)> { + Ok(( + FileTime::from_last_access_time(metadata), + FileTime::from_last_modification_time(metadata), + )) +} + +/// `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. +#[cfg(target_os = "wasi")] +fn atime_mtime_of(metadata: &fs::Metadata) -> std::io::Result<(FileTime, FileTime)> { Ok(( - FileTime::from_last_access_time(&metadata), - FileTime::from_last_modification_time(&metadata), + FileTime::from(metadata.accessed()?), + FileTime::from(metadata.modified()?), )) }