Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 38 additions & 16 deletions src/uu/touch/src/touch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@

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;
Expand Down Expand Up @@ -607,41 +609,61 @@

#[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, &timestamps).is_ok() {
return Ok(());
}

// Fall back to `utimensat` by path. Unlike `filetime::set_file_times`

Check failure on line 619 in src/uu/touch/src/touch.rs

View workflow job for this annotation

GitHub Actions / Style/spelling (ubuntu-latest, feat_os_unix)

ERROR: `cspell`: Unknown word 'utimensat' (file:'src/uu/touch/src/touch.rs', line:619)
// (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(

Check failure on line 623 in src/uu/touch/src/touch.rs

View workflow job for this annotation

GitHub Actions / Style/spelling (ubuntu-latest, feat_os_unix)

ERROR: `cspell`: Unknown word 'utimensat' (file:'src/uu/touch/src/touch.rs', line:623)
rustix::fs::CWD,
path,
&timestamps,
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, &timestamps).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
Expand Down Expand Up @@ -985,7 +1007,7 @@
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);
Expand Down
26 changes: 26 additions & 0 deletions tests/by-util/test_touch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1109,3 +1109,29 @@
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

Check failure on line 1114 in tests/by-util/test_touch.rs

View workflow job for this annotation

GitHub Actions / Style/spelling (ubuntu-latest, feat_os_unix)

ERROR: `cspell`: Unknown word 'utimensat' (file:'tests/by-util/test_touch.rs', line:1114)
// 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() {

Check failure on line 1119 in tests/by-util/test_touch.rs

View workflow job for this annotation

GitHub Actions / Style/spelling (ubuntu-latest, feat_os_unix)

ERROR: `cspell`: Unknown word 'unwritable' (file:'tests/by-util/test_touch.rs', line:1119)
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);
}
Loading