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
4 changes: 4 additions & 0 deletions src/uu/touch/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ tempfile = { workspace = true }
libc = { workspace = true }
rustix = { workspace = true, features = ["fs"] }

[target.'cfg(target_os = "wasi")'.dependencies]
libc = { workspace = true }
rustix = { workspace = true, features = ["fs"] }

[target.'cfg(target_os = "windows")'.dependencies]
windows-sys = { workspace = true, features = [
"Win32_Storage_FileSystem",
Expand Down
105 changes: 82 additions & 23 deletions src/uu/touch/src/touch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,18 @@

use clap::builder::{PossibleValue, ValueParser};
use clap::{Arg, ArgAction, ArgGroup, ArgMatches, Command};
#[cfg(any(not(unix), target_os = "redox"))]
use filetime::FileTime;
#[cfg(any(not(any(unix, target_os = "wasi")), target_os = "redox"))]
use filetime::set_file_times;
use filetime::{FileTime, set_symlink_file_times};
#[cfg(not(target_os = "wasi"))]
use filetime::set_symlink_file_times;
use jiff::civil::Time;
use jiff::fmt::strtime;
use jiff::tz::TimeZone;
use jiff::{Timestamp, ToSpan, Zoned};
#[cfg(unix)]
use libc::O_NONBLOCK;
#[cfg(unix)]
#[cfg(any(unix, target_os = "wasi"))]
use rustix::fs::Timestamps;
#[cfg(unix)]
use rustix::fs::futimens;
Expand Down Expand Up @@ -594,11 +596,8 @@

// sets the file access and modification times for a file or a symbolic link.
// The filename, access time (atime), and modification time (mtime) are provided as inputs.

if opts.no_deref && !is_stdout {
return set_symlink_file_times(path, atime, mtime).map_err_context(
|| translate!("touch-error-setting-times-of-path", "path" => path.quote()),
);
return set_symlink_times(path, atime, mtime);
}

#[cfg(unix)]
Expand All @@ -622,22 +621,46 @@
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
// write-only open returns ENXIO when there is no reader). Fall through
// to set_times_by_path below, 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()),
)
}
// WASI shares the unix fallback: utimensat never opens the file either,
// and the crate's `set_file_times` isn't available there (see below).
#[cfg(any(unix, target_os = "wasi"))]
return set_times_by_path(path, atime, mtime);

#[cfg(not(any(unix, target_os = "wasi")))]
set_file_times(path, atime, mtime)
.map_err_context(|| translate!("touch-error-setting-times-of-path", "path" => path.quote()))
}

#[cfg(unix)]
/// Set access and modification times on `path` without following symlinks.
#[cfg(not(target_os = "wasi"))]
fn set_symlink_times(path: &Path, atime: FileTime, mtime: FileTime) -> UResult<()> {
set_symlink_file_times(path, atime, mtime)
.map_err_context(|| translate!("touch-error-setting-times-of-path", "path" => path.quote()))
}

/// `filetime::set_symlink_file_times` always fails on WASI (the crate has no
/// WASI-specific backend), but `rustix::fs::utimensat` with
/// `AT_SYMLINK_NOFOLLOW` works there on stable.
#[cfg(target_os = "wasi")]
fn set_symlink_times(path: &Path, atime: FileTime, mtime: FileTime) -> UResult<()> {
let timestamps = build_timestamps(atime, mtime);
rustix::fs::utimensat(
rustix::fs::CWD,
path,
&timestamps,
rustix::fs::AtFlags::SYMLINK_NOFOLLOW,
)
.map_err(Error::from)
.map_err_context(|| translate!("touch-error-setting-times-of-path", "path" => path.quote()))
}

#[cfg(any(unix, target_os = "wasi"))]
/// 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 {
Expand All @@ -653,11 +676,12 @@
}
}

#[cfg(all(unix, not(target_os = "redox")))]
/// Set file times by path using `utimensat`, following symlinks.
#[cfg(all(any(unix, target_os = "wasi"), not(target_os = "redox")))]
/// Set file times by path using `utimensat`.
///
/// This never opens the file, so it does not block on special files such as
/// FIFOs.
/// This never opens the file on Unix, avoiding blocks on FIFOs. On WASI, if
/// `utimensat` fails on a non-symlink (e.g. unopenable mode 0 files), it retries

Check warning on line 683 in src/uu/touch/src/touch.rs

View workflow job for this annotation

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

WARNING: `cspell`: Unknown word 'unopenable' (file:'src/uu/touch/src/touch.rs', line:683)
/// with `SYMLINK_NOFOLLOW`.
fn set_times_by_path(path: &Path, atime: FileTime, mtime: FileTime) -> UResult<()> {
let timestamps = build_timestamps(atime, mtime);
rustix::fs::utimensat(
Expand All @@ -666,6 +690,18 @@
&timestamps,
rustix::fs::AtFlags::empty(),
)
.or_else(|err| {
if cfg!(target_os = "wasi") && !path.is_symlink() {
rustix::fs::utimensat(
rustix::fs::CWD,
path,
&timestamps,
rustix::fs::AtFlags::SYMLINK_NOFOLLOW,
)
} else {
Err(err)
}
})
.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()))
}
Expand Down Expand Up @@ -725,9 +761,32 @@
fs::symlink_metadata(path)?
};

atime_mtime_of(&metadata)
}

/// Extract the access and modification `FileTime`s from `metadata`.
///
/// Returns a `Result` (rather than the pair directly) to share a signature
/// with the WASI variant below, whose `Metadata::accessed`/`modified` calls
/// can fail.
#[cfg(not(target_os = "wasi"))]
#[expect(clippy::unnecessary_wraps)]
fn atime_mtime_of(metadata: &fs::Metadata) -> std::io::Result<(FileTime, FileTime)> {
Ok((
FileTime::from_last_access_time(metadata),
FileTime::from_last_modification_time(metadata),
))
}

/// `filetime::FileTime::from_last_{access,modification}_time` panics on WASI
/// (the `filetime` crate has no WASI-specific backend and falls back to its
/// unimplemented generic wasm one). `Metadata::accessed`/`modified` are
/// stable and WASI-backed, so use those instead there.
#[cfg(target_os = "wasi")]
fn atime_mtime_of(metadata: &fs::Metadata) -> std::io::Result<(FileTime, FileTime)> {
Ok((
FileTime::from_last_access_time(&metadata),
FileTime::from_last_modification_time(&metadata),
FileTime::from(metadata.accessed()?),
FileTime::from(metadata.modified()?),
))
}

Expand Down
Loading