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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions fuzz/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 4 additions & 5 deletions src/uu/cp/src/cp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1887,11 +1887,10 @@ pub(crate) fn copy_attributes(
handle_preserve(attributes.timestamps, || -> CopyResult<()> {
let atime = FileTime::from_last_access_time(&source_metadata);
let mtime = FileTime::from_last_modification_time(&source_metadata);
if dest.is_symlink() {
filetime::set_symlink_file_times(dest, atime, mtime)?;
} else {
filetime::set_file_times(dest, atime, mtime)?;
}
// Use uucore's path-based setter rather than `filetime::set_file_times`,
// which opens the destination first and so blocks forever on a FIFO with
// no writer, hanging `cp -a` on a named pipe.
uucore::fs::set_file_times(dest, atime, mtime, !dest.is_symlink())?;

Ok(())
})?;
Expand Down
2 changes: 1 addition & 1 deletion src/uu/touch/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ clap = { workspace = true }
jiff = { workspace = true }
parse_datetime = { workspace = true }
thiserror = { workspace = true }
uucore = { workspace = true, features = ["libc", "parser"] }
uucore = { workspace = true, features = ["fs", "libc", "parser"] }
fluent = { workspace = true }

[dev-dependencies]
Expand Down
48 changes: 10 additions & 38 deletions src/uu/touch/src/touch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ pub mod error;

use clap::builder::{PossibleValue, ValueParser};
use clap::{Arg, ArgAction, ArgGroup, ArgMatches, Command};
#[cfg(any(not(unix), target_os = "redox"))]
use filetime::set_file_times;
use filetime::{FileTime, set_symlink_file_times};
use filetime::FileTime;
use jiff::civil::Time;
use jiff::fmt::strtime;
use jiff::tz::TimeZone;
Expand Down Expand Up @@ -602,7 +600,7 @@ fn update_times(
// 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(
return uucore::fs::set_file_times(path, atime, mtime, false).map_err_context(
|| translate!("touch-error-setting-times-of-path", "path" => path.quote()),
);
}
Expand All @@ -628,16 +626,18 @@ fn update_times(
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)
// write-only open returns ENXIO when there is no reader). Set the
// times by path (never opens the target, unlike
// `filetime::set_file_times`), so it does not block on a FIFO with
// no writer.
uucore::fs::set_file_times(path, atime, mtime, true).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(
uucore::fs::set_file_times(path, atime, mtime, true).map_err_context(
|| translate!("touch-error-setting-times-of-path", "path" => path.quote()),
)
}
Expand All @@ -659,34 +659,6 @@ fn build_timestamps(atime: FileTime, mtime: FileTime) -> Timestamps {
}
}

#[cfg(all(unix, not(target_os = "redox")))]
/// 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(target_os = "redox")]
/// Set file times by path on Redox, which lacks `rustix::fs::utimensat`.
///
/// Falls back to `filetime::set_file_times`; unlike on other unixes this may
/// block on a reader-less FIFO, but Redox has no FIFO support so the FIFO
/// edge case the `utimensat` path guards against does not arise here.
fn set_times_by_path(path: &Path, atime: FileTime, mtime: FileTime) -> UResult<()> {
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`.
///
Expand Down
3 changes: 2 additions & 1 deletion src/uucore/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ uucore_procs = { workspace = true }
unit-prefix = { workspace = true, optional = true }
dns-lookup = { workspace = true, optional = true }
dunce = { version = "1.0.4", optional = true }
filetime = { workspace = true, optional = true }
glob = { workspace = true, optional = true }
itertools = { workspace = true, optional = true }
jiff = { workspace = true, optional = true, features = [
Expand Down Expand Up @@ -142,7 +143,7 @@ encoding = ["data-encoding", "data-encoding-macro", "z85", "base64-simd"]
entries = ["libc"]
extendedbigdecimal = ["bigdecimal", "num-traits"]
fast-inc = []
fs = ["dunce", "libc", "winapi-util", "windows-sys"]
fs = ["dunce", "filetime", "libc", "winapi-util", "windows-sys"]
fsext = ["libc", "windows-sys", "bstr"]
fsxattr = ["xattr", "itertools", "libc"]
hardware = []
Expand Down
55 changes: 54 additions & 1 deletion src/uucore/src/lib/features/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@

//! Set of functions to manage regular files, special files, and links.

// spell-checker:ignore backport
// spell-checker:ignore backport utimensat FDCWD

use filetime::FileTime;
#[cfg(all(unix, not(target_os = "redox")))]
pub use libc::{major, makedev, minor};
use std::collections::HashSet;
Expand Down Expand Up @@ -902,6 +903,58 @@ pub fn makedev(maj: libc::c_uint, min: libc::c_uint) -> libc::dev_t {
(min & 0xff) | ((maj & 0xfff) << 8) | ((min & !0xff) << 12) | ((maj & !0xfff) << 32)
}

/// Build a rustix [`Timestamps`](rustix::fs::Timestamps) from an access and a
/// modification [`FileTime`].
#[cfg(all(unix, not(target_os = "redox")))]
fn to_timestamps(atime: FileTime, mtime: FileTime) -> rustix::fs::Timestamps {
rustix::fs::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 _,
},
}
}

/// Set the access and modification times of `path` without opening it.
///
/// On Unix this uses a path-based `utimensat` (relative to `AT_FDCWD`), matching
/// GNU. `filetime::set_file_times` instead opens the target first, which blocks
/// forever on a FIFO that has no writer — hanging e.g. `cp -a` or `touch` on a
/// named pipe. When `follow_symlinks` is false the times are set on a symlink
/// itself rather than its target.
pub fn set_file_times(
path: &Path,
atime: FileTime,
mtime: FileTime,
follow_symlinks: bool,
) -> IOResult<()> {
#[cfg(all(unix, not(target_os = "redox")))]
{
use rustix::fs::{AtFlags, CWD, utimensat};

let timestamps = to_timestamps(atime, mtime);
let flags = if follow_symlinks {
AtFlags::empty()
} else {
AtFlags::SYMLINK_NOFOLLOW
};
utimensat(CWD, path, &timestamps, flags)
.map_err(|e| Error::from_raw_os_error(e.raw_os_error()))
}
#[cfg(not(all(unix, not(target_os = "redox"))))]
{
if follow_symlinks {
filetime::set_file_times(path, atime, mtime)
} else {
filetime::set_symlink_file_times(path, atime, mtime)
}
}
}

#[cfg(test)]
mod tests {
// Note this useful idiom: importing names from outer (for mod tests) scope.
Expand Down
15 changes: 15 additions & 0 deletions tests/by-util/test_cp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3192,6 +3192,21 @@ fn test_cp_fifo() {
assert_eq!(permission, "prwx-wx--x".to_string());
}

// `cp -a` preserves timestamps; setting them must not open the FIFO (which
// would block forever waiting for a writer). Regression test for that hang.
#[test]
#[cfg(unix)]
fn test_cp_archive_fifo_no_hang() {
let (at, mut ucmd) = at_and_ucmd!();
at.mkfifo("fifo");
ucmd.arg("-a")
.arg("fifo")
.arg("fifo_copy")
.succeeds()
.no_output();
assert!(at.is_fifo("fifo_copy"));
}

#[rstest]
#[case::recursive("-R")]
#[case::archive("-a")]
Expand Down
11 changes: 11 additions & 0 deletions tests/by-util/test_touch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,17 @@ fn test_touch_no_dereference() {
assert_eq!(mtime, start_of_year);
}

// Touching a FIFO must not block: setting times must not open the pipe
// (which would wait for a writer). Regression test for that hang.
#[test]
#[cfg(unix)]
fn test_touch_fifo_no_hang() {
let (at, mut ucmd) = at_and_ucmd!();
at.mkfifo("fifo");
ucmd.arg("fifo").succeeds().no_output();
assert!(at.is_fifo("fifo"));
}

#[test]
fn test_touch_reference() {
let scenario = TestScenario::new(util_name!());
Expand Down
Loading