diff --git a/src/uu/touch/src/touch.rs b/src/uu/touch/src/touch.rs index 4d1c9772c59..b667ffb8963 100644 --- a/src/uu/touch/src/touch.rs +++ b/src/uu/touch/src/touch.rs @@ -10,7 +10,9 @@ 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(not(unix))] +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,41 +609,61 @@ fn update_times( #[cfg(unix)] { + let timestamps = to_timestamps(atime, mtime); + // 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 !is_stdout && try_futimens_via_write_fd(path, ×tamps).is_ok() { return Ok(()); } + + // Fall back to `utimensat` by path. Unlike `filetime::set_file_times` + // (which opens the file to call `futimens` on the fd), this only + // requires ownership of the file, so it still succeeds when the file is + // neither readable nor writable (e.g. mode 0). + 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(not(unix))] set_file_times(path, atime, mtime) .map_err_context(|| translate!("touch-error-setting-times-of-path", "path" => path.quote())) } +/// Build a rustix [`Timestamps`] from access and modification [`FileTime`]s. +#[cfg(unix)] +fn to_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(unix)] /// Set file times via file descriptor using `futimens`. /// /// This opens the file write-only and uses the POSIX `futimens` call to set /// access and modification times on the open FD (not by path), which also /// triggers `IN_CLOSE_WRITE` on Linux when the FD is closed. -fn try_futimens_via_write_fd(path: &Path, atime: FileTime, mtime: FileTime) -> std::io::Result<()> { +fn try_futimens_via_write_fd(path: &Path, timestamps: &Timestamps) -> std::io::Result<()> { let file = OpenOptions::new() .write(true) // Avoid blocking on special files (e.g. FIFOs) before we can inspect metadata. .custom_flags(O_NONBLOCK) .open(path)?; - let 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 _, - }, - }; - - futimens(&file, ×tamps).map_err(|e| Error::from_raw_os_error(e.raw_os_error())) + futimens(&file, timestamps).map_err(|e| Error::from_raw_os_error(e.raw_os_error())) } /// Get metadata of the provided path @@ -985,7 +1007,7 @@ mod tests { let atime = FileTime::from_unix_time(1_600_000_000, 123_456_789); let mtime = FileTime::from_unix_time(1_600_000_100, 987_654_321); - super::try_futimens_via_write_fd(&path, atime, mtime).unwrap(); + super::try_futimens_via_write_fd(&path, &super::to_timestamps(atime, mtime)).unwrap(); let metadata = std::fs::metadata(&path).unwrap(); let actual_atime = FileTime::from_last_access_time(&metadata); diff --git a/tests/by-util/test_touch.rs b/tests/by-util/test_touch.rs index fbabdb75c65..cb84c72c340 100644 --- a/tests/by-util/test_touch.rs +++ b/tests/by-util/test_touch.rs @@ -1109,3 +1109,29 @@ fn test_touch_through_dangling_symlink_creates_target() { assert!(at.file_exists("missing")); assert_eq!(at.read("missing"), ""); } + +// touch must be able to update the times of an owned file that is neither +// readable nor writable (mode 0). Setting explicit times via utimensat-by-path +// only requires ownership, so this succeeds even though the file cannot be +// opened. Regression test for GNU tests/touch/no-rights.sh. +#[test] +#[cfg(unix)] +fn test_touch_set_time_on_unreadable_unwritable_file() { + use std::os::unix::fs::PermissionsExt; + + let (at, mut ucmd) = at_and_ucmd!(); + let file = "no_rights"; + at.touch(file); + std::fs::set_permissions(at.plus(file), std::fs::Permissions::from_mode(0o000)).unwrap(); + + ucmd.args(&["-d", "2000-01-03 00:00", "-c", file]) + .succeeds() + .no_output(); + + // Restore permissions so the test harness can read back the metadata. + std::fs::set_permissions(at.plus(file), std::fs::Permissions::from_mode(0o644)).unwrap(); + let expected = str_to_filetime("%Y-%m-%d %H:%M", "2000-01-03 00:00"); + let (atime, mtime) = get_file_times(&at, file); + assert_eq!(atime, expected); + assert_eq!(mtime, expected); +}