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: 2 additions & 2 deletions .github/workflows/wasi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ jobs:
# arch b2sum cat cksum cp csplit date dir dircolors fmt join
# ls md5sum mkdir mv nproc pathchk pr printenv ptx pwd readlink
# realpath rm rmdir seq sha1sum sha224sum sha256sum sha384sum
# sha512sum shred sleep sort split tail touch tsort uname uniq
# sha512sum shred sleep sort split tail tsort uname uniq
# vdir
UUTESTS_BINARY_PATH="$(pwd)/target/${{ matrix.job.target }}/debug/coreutils.wasm" \
UUTESTS_WASM_RUNNER=wasmtime \
Expand All @@ -73,5 +73,5 @@ jobs:
test_expand:: test_factor:: test_false:: test_fold:: \
test_head:: test_link:: test_ln:: test_nl:: test_numfmt:: \
test_od:: test_paste:: test_printf:: test_shuf:: test_sum:: \
test_tee:: test_tr:: test_true:: test_truncate:: \
test_tee:: test_touch:: test_tr:: test_true:: test_truncate:: \
test_unexpand:: test_unlink:: test_wc:: test_yes::
2 changes: 2 additions & 0 deletions src/uu/touch/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ tempfile = { workspace = true }

[target.'cfg(unix)'.dependencies]
libc = { workspace = true }

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

[target.'cfg(target_os = "windows")'.dependencies]
Expand Down
1 change: 1 addition & 0 deletions src/uu/touch/locales/en-US.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ touch-error-unable-to-parse-date = Unable to parse date: { $date }
touch-error-windows-stdout-path-failed = GetFinalPathNameByHandleW failed with code { $code }
touch-error-invalid-filetime = Source has invalid access or modification time: { $time }
touch-error-reference-file-inaccessible = failed to get attributes of { $path }: { $error }
touch-error-stdout-unsupported = touch - (stdout) is not supported on WASI
1 change: 1 addition & 0 deletions src/uu/touch/locales/fr-FR.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ touch-error-unable-to-parse-date = Impossible d'analyser la date : { $date }
touch-error-windows-stdout-path-failed = GetFinalPathNameByHandleW a échoué avec le code { $code }
touch-error-invalid-filetime = La source a un temps d'accès ou de modification invalide : { $time }
touch-error-reference-file-inaccessible = échec d'obtention des attributs de { $path } : { $error }
touch-error-stdout-unsupported = touch - (sortie standard) n'est pas pris en charge sur WASI
4 changes: 4 additions & 0 deletions src/uu/touch/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ pub enum TouchError {
#[error("{}", translate!("touch-error-windows-stdout-path-failed", "code" => .0.clone()))]
WindowsStdoutPathError(String),

/// A feature that is not available on the current platform
#[error("{0}")]
UnsupportedPlatformFeature(String),

/// An error encountered on a specific file
#[error("{error}")]
TouchFileError {
Expand Down
73 changes: 66 additions & 7 deletions src/uu/touch/src/touch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ pub mod error;

use clap::builder::{PossibleValue, ValueParser};
use clap::{Arg, ArgAction, ArgGroup, ArgMatches, Command};
#[cfg(any(not(unix), target_os = "redox"))]
use filetime::FileTime;
#[cfg(all(any(not(unix), target_os = "redox"), not(target_os = "wasi")))]
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;
Expand Down Expand Up @@ -708,6 +710,47 @@ fn try_futimens_via_write_fd(path: &Path, atime: FileTime, mtime: FileTime) -> s
futimens(&file, &timestamps).map_err(|e| Error::from_raw_os_error(e.raw_os_error()))
}

/// WASI replacement for `filetime::set_file_times`.
///
/// The `filetime` crate has an unimplemented stub on `wasm32-wasi`. WASI
/// supports setting both atime and mtime via `utimensat`, which we reach
/// through `rustix`.
#[cfg(target_os = "wasi")]
fn set_file_times(path: &Path, atime: FileTime, mtime: FileTime) -> std::io::Result<()> {
wasi_utimensat(path, atime, mtime, false)
}

/// WASI replacement for `filetime::set_symlink_file_times`.
#[cfg(target_os = "wasi")]
fn set_symlink_file_times(path: &Path, atime: FileTime, mtime: FileTime) -> std::io::Result<()> {
wasi_utimensat(path, atime, mtime, true)
}

#[cfg(target_os = "wasi")]
fn wasi_utimensat(
path: &Path,
atime: FileTime,
mtime: FileTime,
no_follow: bool,
) -> std::io::Result<()> {
let 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 _,
},
};
let flags = if no_follow {
rustix::fs::AtFlags::SYMLINK_NOFOLLOW
} else {
rustix::fs::AtFlags::empty()
};
rustix::fs::utimensat(rustix::fs::CWD, path, &timestamps, flags).map_err(Error::from)
}

/// Get metadata of the provided path
/// If `follow` is `true`, the function will try to follow symlinks. Errors if the symlink is dangling, otherwise defaults to symlink metadata.
/// If `follow` is `false`, the function will return metadata of the symlink itself
Expand All @@ -725,6 +768,19 @@ fn stat(path: &Path, follow: bool) -> std::io::Result<(FileTime, FileTime)> {
fs::symlink_metadata(path)?
};

// `FileTime::from_last_{access,modification}_time` is unimplemented on
// `wasm32-wasi`, so go through `Metadata::{accessed, modified}` (which
// return `SystemTime`) and convert via `FileTime::from_system_time`.
#[cfg(target_os = "wasi")]
{
let atime = metadata.accessed()?;
let mtime = metadata.modified()?;
Ok((
FileTime::from_system_time(atime),
FileTime::from_system_time(mtime),
))
}
#[cfg(not(target_os = "wasi"))]
Ok((
FileTime::from_last_access_time(&metadata),
FileTime::from_last_modification_time(&metadata),
Expand Down Expand Up @@ -888,7 +944,10 @@ fn parse_timestamp(s: &str) -> UResult<FileTime> {
///
/// On Windows, uses `GetFinalPathNameByHandleW` to attempt to get the path
/// from the stdout handle.
#[cfg_attr(not(windows), expect(clippy::unnecessary_wraps))]
#[cfg_attr(
not(any(windows, target_os = "wasi")),
expect(clippy::unnecessary_wraps)
)]
fn pathbuf_from_stdout() -> Result<PathBuf, TouchError> {
#[cfg(all(unix, not(target_os = "android")))]
{
Expand All @@ -898,6 +957,10 @@ fn pathbuf_from_stdout() -> Result<PathBuf, TouchError> {
{
Ok(PathBuf::from("/proc/self/fd/1"))
}
#[cfg(target_os = "wasi")]
return Err(TouchError::UnsupportedPlatformFeature(translate!(
"touch-error-stdout-unsupported"
)));
#[cfg(windows)]
{
use std::os::windows::prelude::AsRawHandle;
Expand Down Expand Up @@ -954,10 +1017,6 @@ fn pathbuf_from_stdout() -> Result<PathBuf, TouchError> {
.map_err(|e| TouchError::WindowsStdoutPathError(e.to_string()))?
.into())
}
#[cfg(target_os = "wasi")]
{
Ok(PathBuf::from("/dev/stdout"))
}
}

#[cfg(test)]
Expand Down
64 changes: 58 additions & 6 deletions tests/by-util/test_touch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
// spell-checker:ignore (formats) cymdhm cymdhms datetime mdhm mdhms mktime strtime ymdhm ymdhms
// spell-checker:ignore (formats) cymdhm cymdhms datetime filestat mdhm mdhms mktime preopen strtime tzdb ymdhm ymdhms

use filetime::FileTime;
#[cfg(not(target_os = "freebsd"))]
Expand Down Expand Up @@ -166,6 +166,10 @@ fn test_touch_2_digit_years_2038() {
}

#[test]
#[cfg_attr(
wasi_runner,
ignore = "WASI: pre-epoch timestamps not representable by path_filestat_set_times"
)]
fn test_touch_2_digit_years_69() {
// 69 and after are 19xx
let (at, mut ucmd) = at_and_ucmd!();
Expand Down Expand Up @@ -623,6 +627,10 @@ fn test_touch_set_date7() {
}

#[test]
#[cfg_attr(
wasi_runner,
ignore = "WASI: no tzdb; TZ env var is not honoured so timezone-dependent timestamps differ"
)]
fn test_touch_set_date_without_leading_zeroes() {
let (at, mut ucmd) = at_and_ucmd!();
let file = "test_touch_set_date_without_leading_zeroes";
Expand All @@ -646,6 +654,10 @@ fn test_touch_set_date_without_leading_zeroes() {
/// (which uses i64 `tv_sec` natively), this should succeed on all targets.
#[test]
#[cfg(unix)]
#[cfg_attr(
wasi_runner,
ignore = "WASI: pre-epoch timestamps not representable by path_filestat_set_times"
)]
fn test_touch_set_date_year_zero() {
let (at, mut ucmd) = at_and_ucmd!();
let file = "test_touch_year_zero";
Expand Down Expand Up @@ -780,6 +792,10 @@ fn test_touch_mtime_dst_succeeds() {

#[test]
#[cfg(unix)]
#[cfg_attr(
wasi_runner,
ignore = "WASI: no tzdb; TZ env var is not honoured so DST validation is skipped"
)]
fn test_touch_mtime_dst_fails() {
let file = "test_touch_set_mtime_dst_fails";

Expand All @@ -797,6 +813,10 @@ fn test_touch_mtime_dst_fails() {

#[test]
#[cfg(unix)]
#[cfg_attr(
wasi_runner,
ignore = "WASI: guest root is a writable preopen, not the protected system root"
)]
fn test_touch_system_fails() {
let file = "/";
new_ucmd!()
Expand Down Expand Up @@ -874,6 +894,7 @@ fn test_touch_no_such_file_error_msg() {

#[test]
#[cfg(not(any(target_os = "freebsd", target_os = "openbsd")))]
#[cfg_attr(wasi_runner, ignore = "WASI: touch - (stdout) is unsupported")]
fn test_touch_changes_time_of_file_in_stdout() {
// command like: `touch - 1< ./c`
// should change the timestamp of c
Expand All @@ -896,6 +917,10 @@ fn test_touch_changes_time_of_file_in_stdout() {

#[test]
#[cfg(unix)]
#[cfg_attr(
wasi_runner,
ignore = "WASI: filesystem permission errors surface as ENOENT rather than EACCES"
)]
fn test_touch_permission_denied_error_msg() {
let (at, mut ucmd) = at_and_ucmd!();

Expand Down Expand Up @@ -1016,10 +1041,20 @@ fn test_touch_no_dereference_dangling() {

#[test]
#[cfg(not(target_os = "openbsd"))]
#[cfg_attr(wasi_runner, ignore = "WASI: touch - (stdout) is unsupported")]
fn test_touch_dash() {
new_ucmd!().args(&["-h", "-"]).succeeds().no_output();
}

#[test]
#[cfg(wasi_runner)]
fn test_touch_dash_unsupported() {
new_ucmd!()
.arg("-")
.fails_with_code(1)
.stderr_only("touch: touch - (stdout) is not supported on WASI\n");
}

#[test]
fn test_touch_invalid_date_format() {
let file = "test_touch_invalid_date_format";
Expand Down Expand Up @@ -1056,15 +1091,22 @@ fn test_touch_symlink_with_no_deref() {
let (at, mut ucmd) = at_and_ucmd!();
let target = "foo.txt";
let symlink = "bar.txt";
let time = FileTime::from_unix_time(123, 0);
let initial_time = FileTime::from_unix_time(123, 0);
let updated_atime = FileTime::from_unix_time(456, 0);

at.touch(target);
let target_times = get_file_times(&at, target);
at.relative_symlink_file(target, symlink);
set_symlink_file_times(at.plus(symlink), time, time).unwrap();
set_symlink_file_times(at.plus(symlink), initial_time, initial_time).unwrap();

ucmd.args(&["-a", "--no-dereference", "-d", "@456", symlink])
.succeeds();

ucmd.args(&["-a", "--no-dereference", symlink]).succeeds();
// Modification time shouldn't be set to the destination's modification time
assert_eq!(time, get_symlink_times(&at, symlink).1);
assert_eq!(
(updated_atime, initial_time),
get_symlink_times(&at, symlink)
);
assert_eq!(target_times, get_file_times(&at, target));
}

#[test]
Expand Down Expand Up @@ -1124,6 +1166,7 @@ fn test_touch_f_option() {

#[test]
#[cfg(target_os = "linux")]
#[cfg_attr(wasi_runner, ignore = "WASI: argv/filenames must be valid UTF-8")]
fn test_touch_non_utf8_paths() {
use std::ffi::OsStr;
use std::os::unix::ffi::OsStrExt;
Expand All @@ -1140,6 +1183,7 @@ fn test_touch_non_utf8_paths() {

#[test]
#[cfg(target_os = "linux")]
#[cfg_attr(wasi_runner, ignore = "WASI sandbox: host paths not visible")]
fn test_touch_device_files() {
let (_, mut ucmd) = at_and_ucmd!();
ucmd.args(&["/dev/null", "/dev/zero", "/dev/full", "/dev/random"])
Expand All @@ -1155,6 +1199,10 @@ fn test_touch_device_files() {
// check in util/check-safe-traversal.sh.
#[test]
#[cfg(unix)]
#[cfg_attr(
wasi_runner,
ignore = "WASI sandbox: absolute symlink targets cannot be followed"
)]
fn test_touch_does_not_truncate_symlink_target() {
use std::os::unix::fs::symlink;

Expand All @@ -1170,6 +1218,10 @@ fn test_touch_does_not_truncate_symlink_target() {
// Touching a dangling symlink creates its target as an empty file, like GNU.
#[test]
#[cfg(unix)]
#[cfg_attr(
wasi_runner,
ignore = "WASI sandbox: absolute symlink targets cannot be followed"
)]
fn test_touch_through_dangling_symlink_creates_target() {
use std::os::unix::fs::symlink;

Expand Down
Loading