diff --git a/src/uu/touch/src/touch.rs b/src/uu/touch/src/touch.rs index 4d1c9772c59..c8e092f4451 100644 --- a/src/uu/touch/src/touch.rs +++ b/src/uu/touch/src/touch.rs @@ -4,13 +4,15 @@ // file that was distributed with this source code. // spell-checker:ignore (ToDO) datelike datetime filetime lpszfilepath mktime strtime timelike utime DATETIME UTIME futimens -// spell-checker:ignore (FORMATS) MMDDhhmm YYYYMMDDHHMM YYMMDDHHMM YYYYMMDDHHMMS CREAT +// spell-checker:ignore (FORMATS) MMDDhhmm YYYYMMDDHHMM YYMMDDHHMM YYYYMMDDHHMMS CREAT ENXIO RDONLY utimensat pub mod error; use clap::builder::{PossibleValue, ValueParser}; use clap::{Arg, ArgAction, ArgGroup, ArgMatches, Command}; -use filetime::{FileTime, set_file_times, set_symlink_file_times}; +#[cfg(any(not(unix), target_os = "redox"))] +use filetime::set_file_times; +use filetime::{FileTime, set_symlink_file_times}; use jiff::civil::Time; use jiff::fmt::strtime; use jiff::tz::TimeZone; @@ -607,12 +609,80 @@ fn update_times( #[cfg(unix)] { + if is_stdout { + // `touch -` operates on whatever file is open as stdout (fd 1), + // even when it was opened read-only. Use futimens on the fd + // directly: it preserves the UTIME_NOW sentinel, while + // filetime::set_file_times would normalize it into a literal + // 1970 timestamp. + let timestamps = build_timestamps(atime, mtime); + return futimens(std::io::stdout(), ×tamps) + .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()), + ); + } + // Open write-only and use futimens to trigger IN_CLOSE_WRITE on Linux. - if !is_stdout && try_futimens_via_write_fd(path, atime, mtime).is_ok() { + if try_futimens_via_write_fd(path, atime, mtime).is_ok() { 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 + // 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()), + ) + } +} + +#[cfg(unix)] +/// 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 { + Timestamps { + last_access: rustix::fs::Timespec { + tv_sec: atime.unix_seconds(), + tv_nsec: atime.nanoseconds() as _, + }, + last_modification: rustix::fs::Timespec { + tv_sec: mtime.unix_seconds(), + tv_nsec: mtime.nanoseconds() as _, + }, + } +} + +#[cfg(all(unix, not(target_os = "redox")))] +/// Set file times by path using `utimensat`, following symlinks. +/// +/// This never opens the file, so it does not block on special files such as +/// FIFOs. +fn set_times_by_path(path: &Path, atime: FileTime, mtime: FileTime) -> UResult<()> { + let timestamps = build_timestamps(atime, mtime); + rustix::fs::utimensat( + rustix::fs::CWD, + path, + ×tamps, + rustix::fs::AtFlags::empty(), + ) + .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())) +} + +#[cfg(target_os = "redox")] +/// Set file times by path on Redox, which lacks `rustix::fs::utimensat`. +/// +/// Falls back to `filetime::set_file_times`; unlike on other unixes this may +/// block on a reader-less FIFO, but Redox has no FIFO support so the FIFO +/// edge case the `utimensat` path guards against does not arise here. +fn set_times_by_path(path: &Path, atime: FileTime, mtime: FileTime) -> UResult<()> { set_file_times(path, atime, mtime) .map_err_context(|| translate!("touch-error-setting-times-of-path", "path" => path.quote())) } diff --git a/tests/by-util/test_touch.rs b/tests/by-util/test_touch.rs index fbabdb75c65..59f9b773eed 100644 --- a/tests/by-util/test_touch.rs +++ b/tests/by-util/test_touch.rs @@ -789,6 +789,42 @@ fn test_touch_system_fails() { .stderr_contains("setting times of '/'"); } +#[test] +#[cfg(unix)] +#[cfg_attr(wasi_runner, ignore = "WASI: no FIFO support")] +fn test_touch_fifo() { + // touch must not hang on a reader-less FIFO and must update its times. + let (at, mut ucmd) = at_and_ucmd!(); + at.mkfifo("fifo"); + ucmd.args(&["-d", "2020-01-01 00:00:00", "fifo"]) + .succeeds() + .no_output(); + assert!(at.is_fifo("fifo")); +} + +#[test] +#[cfg(unix)] +#[cfg_attr(wasi_runner, ignore = "WASI: no stdout-to-file redirection")] +fn test_touch_dash_updates_stdout_file() { + // `touch -` must update the times of the file open as stdout (fd 1), even + // when it is read-only, and set them to "now" rather than a 1970 sentinel. + use std::fs::File; + use std::time::SystemTime; + + let (at, mut ucmd) = at_and_ucmd!(); + at.touch("c"); + // Age the file so the update to "now" is detectable. + let old = FileTime::from_unix_time(1_000_000, 0); + filetime::set_file_times(at.plus("c"), old, old).unwrap(); + + let file = File::open(at.plus("c")).unwrap(); + ucmd.set_stdout(file).arg("-").succeeds(); + + let mtime = at.metadata("c").modified().unwrap(); + let age = SystemTime::now().duration_since(mtime).unwrap(); + assert!(age.as_secs() < 60, "touch - left mtime stale: {age:?}"); +} + #[test] #[cfg(not(target_os = "windows"))] fn test_touch_trailing_slash() {