From 06866afa96ced8e493449d1dd22fa81ffc8f1590 Mon Sep 17 00:00:00 2001 From: mattsu Date: Wed, 24 Dec 2025 18:45:35 +0900 Subject: [PATCH 1/3] feat(touch): use futimens via write fd to trigger IN_CLOSE_WRITE on Linux - Add `try_futimens_via_write_fd` function for Unix systems to open files write-only and set times using `futimens`, ensuring inotify watchers detect file closure after touch. - Modify `update_times` to attempt this method before falling back to `set_file_times`, improving compatibility with file monitoring tools on Linux. - Include necessary imports for Unix-specific operations. --- src/uu/touch/src/touch.rs | 49 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/src/uu/touch/src/touch.rs b/src/uu/touch/src/touch.rs index 90676d21f0d..b1af4f5a2ae 100644 --- a/src/uu/touch/src/touch.rs +++ b/src/uu/touch/src/touch.rs @@ -18,11 +18,17 @@ use filetime::{FileTime, set_file_times, set_symlink_file_times}; use jiff::{Timestamp, Zoned}; use std::borrow::Cow; use std::ffi::OsString; +#[cfg(unix)] +use std::fs::OpenOptions; use std::fs::{self, File}; use std::io::{Error, ErrorKind}; +#[cfg(unix)] +use std::os::unix::io::AsRawFd; use std::path::{Path, PathBuf}; use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError}; +#[cfg(unix)] +use uucore::libc; use uucore::parser::shortcut_value_parser::ShortcutValueParser; use uucore::translate; use uucore::{format_usage, show}; @@ -566,11 +572,48 @@ 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) + 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(()); + } + } + + set_file_times(path, atime, mtime) + .map_err_context(|| translate!("touch-error-setting-times-of-path", "path" => path.quote())) +} + +#[cfg(unix)] +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 times = [ + libc::timespec { + tv_sec: atime.unix_seconds() as libc::time_t, + tv_nsec: atime.nanoseconds() as libc::c_long, + }, + libc::timespec { + tv_sec: mtime.unix_seconds() as libc::time_t, + tv_nsec: mtime.nanoseconds() as libc::c_long, + }, + ]; + + let rc = unsafe { libc::futimens(file.as_raw_fd(), times.as_ptr()) }; + if rc == 0 { + Ok(()) } else { - set_file_times(path, atime, mtime) + Err(std::io::Error::last_os_error()) } - .map_err_context(|| translate!("touch-error-setting-times-of-path", "path" => path.quote())) } /// Get metadata of the provided path From 27dfb9a649fa5d6f12943de3222e925beb1118cd Mon Sep 17 00:00:00 2001 From: mattsu Date: Wed, 24 Dec 2025 18:53:18 +0900 Subject: [PATCH 2/3] chore: add 'futimens' to spell-checker ignore list in touch.rs - Updated the spell-checker comment to include 'futimens', likely a system call or function name, to avoid false positives in code spell-checking. --- src/uu/touch/src/touch.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uu/touch/src/touch.rs b/src/uu/touch/src/touch.rs index b1af4f5a2ae..67a195124db 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; From 028aa01108c0e7dcad413e9710886e30f5ed2050 Mon Sep 17 00:00:00 2001 From: mattsu Date: Wed, 24 Dec 2025 19:48:13 +0900 Subject: [PATCH 3/3] refactor(touch): replace libc futimens with nix crate for safer file time setting - Add nix and tempfile dependencies to Cargo.toml and Cargo.lock - Replace unsafe libc::futimens calls with nix::sys::stat::futimens for better safety and portability - Introduce try_futimens_via_write_fd function using nix abstractions - Add unit tests to verify futimens functionality on Unix systems This change reduces unsafe code usage and leverages a Rust-friendly library for Unix-specific operations. --- Cargo.lock | 2 ++ src/uu/touch/Cargo.toml | 6 ++++ src/uu/touch/src/touch.rs | 75 ++++++++++++++++++++++++++++----------- 3 files changed, 62 insertions(+), 21 deletions(-) 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 67a195124db..d24ae4c2f14 100644 --- a/src/uu/touch/src/touch.rs +++ b/src/uu/touch/src/touch.rs @@ -16,19 +16,19 @@ 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}; -#[cfg(unix)] -use std::os::unix::io::AsRawFd; use std::path::{Path, PathBuf}; use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError}; -#[cfg(unix)] -use uucore::libc; use uucore::parser::shortcut_value_parser::ShortcutValueParser; use uucore::translate; use uucore::{format_usage, show}; @@ -590,6 +590,11 @@ fn update_times( } #[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() { @@ -597,23 +602,11 @@ fn try_futimens_via_write_fd(path: &Path, atime: FileTime, mtime: FileTime) -> s } let file = OpenOptions::new().write(true).open(path)?; - let times = [ - libc::timespec { - tv_sec: atime.unix_seconds() as libc::time_t, - tv_nsec: atime.nanoseconds() as libc::c_long, - }, - libc::timespec { - tv_sec: mtime.unix_seconds() as libc::time_t, - tv_nsec: mtime.nanoseconds() as libc::c_long, - }, - ]; - - let rc = unsafe { libc::futimens(file.as_raw_fd(), times.as_ptr()) }; - if rc == 0 { - Ok(()) - } else { - Err(std::io::Error::last_os_error()) - } + 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 @@ -895,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)] @@ -966,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")); + } }