diff --git a/Cargo.lock b/Cargo.lock index 5781d4e32fd..3a43d1ff425 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3989,7 +3989,9 @@ dependencies = [ "filetime", "fluent", "jiff", + "nix", "parse_datetime", + "tempfile", "thiserror 2.0.17", "uucore", "windows-sys 0.61.2", diff --git a/src/uu/touch/Cargo.toml b/src/uu/touch/Cargo.toml index f5409ec7a5a..e180e076278 100644 --- a/src/uu/touch/Cargo.toml +++ b/src/uu/touch/Cargo.toml @@ -28,6 +28,12 @@ thiserror = { workspace = true } uucore = { workspace = true, features = ["libc", "parser"] } fluent = { workspace = true } +[target.'cfg(unix)'.dependencies] +nix = { workspace = true, features = ["fs"] } + +[dev-dependencies] +tempfile = { workspace = true } + [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 90676d21f0d..d24ae4c2f14 100644 --- a/src/uu/touch/src/touch.rs +++ b/src/uu/touch/src/touch.rs @@ -3,7 +3,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (ToDO) filetime datetime lpszfilepath mktime DATETIME datelike timelike +// spell-checker:ignore (ToDO) filetime datetime lpszfilepath mktime DATETIME datelike timelike futimens // spell-checker:ignore (FORMATS) MMDDhhmm YYYYMMDDHHMM YYMMDDHHMM YYYYMMDDHHMMS pub mod error; @@ -16,8 +16,14 @@ use clap::builder::{PossibleValue, ValueParser}; use clap::{Arg, ArgAction, ArgGroup, ArgMatches, Command}; use filetime::{FileTime, set_file_times, set_symlink_file_times}; use jiff::{Timestamp, Zoned}; +#[cfg(unix)] +use nix::sys::stat::futimens; +#[cfg(unix)] +use nix::sys::time::TimeSpec; use std::borrow::Cow; use std::ffi::OsString; +#[cfg(unix)] +use std::fs::OpenOptions; use std::fs::{self, File}; use std::io::{Error, ErrorKind}; use std::path::{Path, PathBuf}; @@ -566,11 +572,41 @@ fn update_times( // The filename, access time (atime), and modification time (mtime) are provided as inputs. if opts.no_deref && !is_stdout { - set_symlink_file_times(path, atime, mtime) - } else { - set_file_times(path, atime, mtime) + return set_symlink_file_times(path, atime, mtime).map_err_context( + || translate!("touch-error-setting-times-of-path", "path" => path.quote()), + ); + } + + #[cfg(unix)] + { + // 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() { + return Ok(()); + } } - .map_err_context(|| translate!("touch-error-setting-times-of-path", "path" => path.quote())) + + set_file_times(path, atime, mtime) + .map_err_context(|| translate!("touch-error-setting-times-of-path", "path" => path.quote())) +} + +#[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<()> { + let metadata = fs::metadata(path)?; + if !metadata.is_file() { + return Err(Error::other("not a regular file")); + } + + let file = OpenOptions::new().write(true).open(path)?; + let atime_spec = TimeSpec::new(atime.unix_seconds() as _, atime.nanoseconds() as _); + let mtime_spec = TimeSpec::new(mtime.unix_seconds() as _, mtime.nanoseconds() as _); + + futimens(&file, &atime_spec, &mtime_spec) + .map_err(|err| std::io::Error::from_raw_os_error(err as i32)) } /// Get metadata of the provided path @@ -852,6 +888,13 @@ mod tests { uu_app, }; + #[cfg(unix)] + use std::fs; + #[cfg(unix)] + use std::io::ErrorKind; + #[cfg(unix)] + use tempfile::tempdir; + #[cfg(windows)] use std::env; #[cfg(windows)] @@ -923,4 +966,37 @@ mod tests { Ok(_) => panic!("Expected to error with TouchError::InvalidFiletime but succeeded"), } } + + #[cfg(unix)] + #[test] + fn test_try_futimens_via_write_fd_sets_times() { + let dir = tempdir().unwrap(); + let path = dir.path().join("futimens-file"); + fs::write(&path, b"data").unwrap(); + + 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(); + + let metadata = fs::metadata(&path).unwrap(); + let actual_atime = FileTime::from_last_access_time(&metadata); + let actual_mtime = FileTime::from_last_modification_time(&metadata); + + assert_eq!(actual_atime, atime); + assert_eq!(actual_mtime, mtime); + } + + #[cfg(unix)] + #[test] + fn test_try_futimens_via_write_fd_rejects_non_file() { + let dir = tempdir().unwrap(); + let atime = FileTime::from_unix_time(1_600_000_000, 0); + let mtime = FileTime::from_unix_time(1_600_000_001, 0); + + let err = super::try_futimens_via_write_fd(dir.path(), atime, mtime) + .expect_err("expected error for non-regular file"); + assert_eq!(err.kind(), ErrorKind::Other); + assert!(err.to_string().contains("not a regular file")); + } }