From 8e88196bf45e1016c75f578421104ec1a9aba967 Mon Sep 17 00:00:00 2001 From: mattsu Date: Wed, 18 Feb 2026 18:11:23 +0900 Subject: [PATCH] touch: set file times via futimens on a write fd On Unix, try futimens through a write-opened file descriptor to trigger IN_CLOSE_WRITE semantics, and fall back to set_file_times when needed. Also adds a unit test for the futimens path and wires unix-only dependencies. --- Cargo.lock | 2 + src/uu/touch/Cargo.toml | 6 +++ src/uu/touch/src/touch.rs | 77 ++++++++++++++++++++++++++++++++++++--- 3 files changed, 79 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dabc2e4fa20..849b56965fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4198,7 +4198,9 @@ dependencies = [ "filetime", "fluent", "jiff", + "nix", "parse_datetime", + "tempfile", "thiserror 2.0.18", "uucore", "windows-sys 0.61.2", diff --git a/src/uu/touch/Cargo.toml b/src/uu/touch/Cargo.toml index 9f66c3795c7..cbcda5ae4a2 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 e50686417e2..c2159785c55 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) datelike datetime filetime lpszfilepath mktime strtime timelike utime +// spell-checker:ignore (ToDO) datelike datetime filetime lpszfilepath mktime strtime timelike utime DATETIME UTIME futimens // spell-checker:ignore (FORMATS) MMDDhhmm YYYYMMDDHHMM YYMMDDHHMM YYYYMMDDHHMMS pub mod error; @@ -15,10 +15,18 @@ use jiff::civil::Time; use jiff::fmt::strtime; use jiff::tz::TimeZone; use jiff::{Timestamp, ToSpan, Zoned}; +#[cfg(unix)] +use nix::sys::stat::futimens; +#[cfg(unix)] +use nix::sys::time::TimeSpec; use std::borrow::Cow; use std::ffi::{OsStr, OsString}; +#[cfg(unix)] +use std::fs::OpenOptions; use std::fs::{self, File}; use std::io::{Error, ErrorKind}; +#[cfg(unix)] +use std::os::unix::fs::OpenOptionsExt; use std::path::{Path, PathBuf}; use std::time::SystemTime; use uucore::display::Quotable; @@ -577,11 +585,45 @@ 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()), + ); } - .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(()); + } + } + + 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 file = OpenOptions::new() + .write(true) + // Avoid blocking on special files (e.g. FIFOs) before we can inspect metadata. + .custom_flags(nix::libc::O_NONBLOCK) + .open(path)?; + + let atime_sec = atime.unix_seconds(); + let atime_nsec = i64::from(atime.nanoseconds()); + let mtime_sec = mtime.unix_seconds(); + let mtime_nsec = i64::from(mtime.nanoseconds()); + + let atime_spec = TimeSpec::new(atime_sec, atime_nsec); + let mtime_spec = TimeSpec::new(mtime_sec, mtime_nsec); + + futimens(&file, &atime_spec, &mtime_spec).map_err(Error::from) } /// Get metadata of the provided path @@ -837,11 +879,13 @@ mod tests { uu_app, }; + #[cfg(unix)] + use tempfile::tempdir; + #[cfg(windows)] use std::env; #[cfg(windows)] use uucore::locale; - #[cfg(windows)] #[test] fn test_get_pathbuf_from_stdout_fails_if_stdout_is_not_a_file() { @@ -908,4 +952,25 @@ 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"); + std::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 = std::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); + } + }