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
69 changes: 64 additions & 5 deletions src/uu/touch/src/touch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
// file that was distributed with this source code.

// spell-checker:ignore (ToDO) datelike datetime filetime lpszfilepath mktime strtime timelike utime DATETIME UTIME futimens
// spell-checker:ignore (FORMATS) MMDDhhmm YYYYMMDDHHMM YYMMDDHHMM YYYYMMDDHHMMS CREAT
// spell-checker:ignore (FORMATS) MMDDhhmm YYYYMMDDHHMM YYMMDDHHMM YYYYMMDDHHMMS CREAT ENXIO RDONLY utimensat

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;
Expand Down Expand Up @@ -607,14 +609,71 @@ fn update_times(

#[cfg(unix)]
{
if is_stdout {
// `touch -` operates on whatever file is open as stdout (fd 1),
// even when it was opened read-only. Use futimens on the fd
// directly: it preserves the UTIME_NOW sentinel, while
// filetime::set_file_times would normalize it into a literal
// 1970 timestamp.
let timestamps = build_timestamps(atime, mtime);
return futimens(std::io::stdout(), &timestamps)
.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()),
);
}

// 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 try_futimens_via_write_fd(path, atime, mtime).is_ok() {
return Ok(());
}
// The write-FD approach fails on special files such as FIFOs (the
// write-only open returns ENXIO when there is no reader). Set the times
// by path with utimensat, which never opens the file and so never
// blocks — unlike filetime::set_file_times, which opens O_RDONLY and
// would hang on a reader-less FIFO.
set_times_by_path(path, atime, mtime)
}

#[cfg(not(unix))]
{
set_file_times(path, atime, mtime).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)]
/// Build a rustix `Timestamps` from the access and modification `FileTime`s,
/// preserving the `UTIME_NOW`/`UTIME_OMIT` sentinels in the nanoseconds field.
fn build_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 by path using `utimensat`, following symlinks.
///
/// This never opens the file, so it does not block on special files such as
/// FIFOs.
fn set_times_by_path(path: &Path, atime: FileTime, mtime: FileTime) -> UResult<()> {
let timestamps = build_timestamps(atime, mtime);
rustix::fs::utimensat(
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(unix)]
Expand Down
36 changes: 36 additions & 0 deletions tests/by-util/test_touch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -789,6 +789,42 @@ fn test_touch_system_fails() {
.stderr_contains("setting times of '/'");
}

#[test]
#[cfg(unix)]
#[cfg_attr(wasi_runner, ignore = "WASI: no FIFO support")]
fn test_touch_fifo() {
// touch must not hang on a reader-less FIFO and must update its times.
let (at, mut ucmd) = at_and_ucmd!();
at.mkfifo("fifo");
ucmd.args(&["-d", "2020-01-01 00:00:00", "fifo"])
.succeeds()
.no_output();
assert!(at.is_fifo("fifo"));
}

#[test]
#[cfg(unix)]
#[cfg_attr(wasi_runner, ignore = "WASI: no stdout-to-file redirection")]
fn test_touch_dash_updates_stdout_file() {
// `touch -` must update the times of the file open as stdout (fd 1), even
// when it is read-only, and set them to "now" rather than a 1970 sentinel.
use std::fs::File;
use std::time::SystemTime;

let (at, mut ucmd) = at_and_ucmd!();
at.touch("c");
// Age the file so the update to "now" is detectable.
let old = FileTime::from_unix_time(1_000_000, 0);
filetime::set_file_times(at.plus("c"), old, old).unwrap();

let file = File::open(at.plus("c")).unwrap();
ucmd.set_stdout(file).arg("-").succeeds();

let mtime = at.metadata("c").modified().unwrap();
let age = SystemTime::now().duration_since(mtime).unwrap();
assert!(age.as_secs() < 60, "touch - left mtime stale: {age:?}");
}

#[test]
#[cfg(not(target_os = "windows"))]
fn test_touch_trailing_slash() {
Expand Down
Loading