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
2 changes: 2 additions & 0 deletions Cargo.lock

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

6 changes: 6 additions & 0 deletions src/uu/touch/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ thiserror = { workspace = true }
uucore = { workspace = true, features = ["libc", "parser"] }
fluent = { workspace = true }

[target.'cfg(unix)'.dependencies]
nix = { workspace = true, features = ["fs"] }

[dev-dependencies]
tempfile = { workspace = true }

[target.'cfg(target_os = "windows")'.dependencies]
windows-sys = { workspace = true, features = [
"Win32_Storage_FileSystem",
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 @@ -19,6 +19,7 @@ touch-error-setting-times-of = setting times of { $filename }
touch-error-setting-times-no-such-file = setting times of { $filename }: No such file or directory
touch-error-cannot-touch = cannot touch { $filename }
touch-error-no-such-file-or-directory = No such file or directory
touch-error-not-a-regular-file = not a regular file
touch-error-failed-to-get-attributes = failed to get attributes of { $path }
touch-error-setting-times-of-path = setting times of { $path }
touch-error-invalid-date-ts-format = invalid date ts format { $date }
Expand Down
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 @@ -20,6 +20,7 @@ touch-error-setting-times-of = définition des temps de { $filename }
touch-error-setting-times-no-such-file = définition des temps de { $filename } : Aucun fichier ou répertoire de ce type
touch-error-cannot-touch = impossible de toucher { $filename }
touch-error-no-such-file-or-directory = Aucun fichier ou répertoire de ce type
touch-error-not-a-regular-file = pas un fichier ordinaire
touch-error-failed-to-get-attributes = échec d'obtention des attributs de { $path }
touch-error-setting-times-of-path = définition des temps de { $path }
touch-error-invalid-date-ts-format = format de date ts invalide { $date }
Expand Down
175 changes: 159 additions & 16 deletions src/uu/touch/src/touch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,30 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.

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

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};
use filetime::{set_file_times, set_symlink_file_times, FileTime};
use jiff::civil::Time;
use jiff::fmt::strtime;
use jiff::tz::TimeZone;
use jiff::{Timestamp, ToSpan, Zoned};
#[cfg(unix)]
use nix::sys::stat::futimens;
#[cfg(unix)]
use nix::sys::time::TimeSpec;
use std::borrow::Cow;
use std::ffi::{OsStr, OsString};
#[cfg(unix)]
use std::fs::OpenOptions;
use std::fs::{self, File};
use std::io::{Error, ErrorKind};
#[cfg(unix)]
use std::os::unix::fs::OpenOptionsExt;
use std::path::{Path, PathBuf};
use std::time::SystemTime;
use uucore::display::Quotable;
Expand Down Expand Up @@ -577,11 +585,82 @@
// The filename, access time (atime), and modification time (mtime) are provided as inputs.

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

#[cfg(unix)]
{
// Open write-only and use futimens to trigger IN_CLOSE_WRITE on Linux.
if !is_stdout {
match try_futimens_via_write_fd(path, atime, mtime) {
Ok(()) => return Ok(()),
Err(futimens_err) => {
return set_file_times(path, atime, mtime)
.map_err(|fallback_err| {
Error::other(format!(
"futimens failed: {}; set_file_times fallback failed: {}",
uucore::error::strip_errno(&futimens_err),
uucore::error::strip_errno(&fallback_err),
))
})
.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)]
/// 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<()> {
let file = OpenOptions::new()
.write(true)
// Avoid blocking on special files (e.g. FIFOs) before we can inspect metadata.
.custom_flags(nix::libc::O_NONBLOCK)
.open(path)
.map_err(|err| {
if err.raw_os_error() == Some(nix::libc::EISDIR) {

Check failure on line 633 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 'EISDIR' (file:'src/uu/touch/src/touch.rs', line:633)
Error::other(translate!("touch-error-not-a-regular-file"))
} else {
err
}
})?;
if !file.metadata()?.is_file() {
return Err(Error::other(translate!("touch-error-not-a-regular-file")));
}
.map_err_context(|| translate!("touch-error-setting-times-of-path", "path" => path.quote()))

let atime_sec = atime
.unix_seconds()
.try_into()
.map_err(|_| Error::from(ErrorKind::InvalidInput))?;
let atime_nsec = atime
.nanoseconds()
.try_into()
.map_err(|_| Error::from(ErrorKind::InvalidInput))?;
let mtime_sec = mtime
.unix_seconds()
.try_into()
.map_err(|_| Error::from(ErrorKind::InvalidInput))?;
let mtime_nsec = mtime
.nanoseconds()
.try_into()
.map_err(|_| Error::from(ErrorKind::InvalidInput))?;

let atime_spec = TimeSpec::new(atime_sec, atime_nsec);
let mtime_spec = TimeSpec::new(mtime_sec, mtime_nsec);

futimens(&file, &atime_spec, &mtime_spec).map_err(Error::from)
}

/// Get metadata of the provided path
Expand Down Expand Up @@ -774,11 +853,11 @@
{
use std::os::windows::prelude::AsRawHandle;
use windows_sys::Win32::Foundation::{
ERROR_INVALID_PARAMETER, ERROR_NOT_ENOUGH_MEMORY, ERROR_PATH_NOT_FOUND, GetLastError,
GetLastError, ERROR_INVALID_PARAMETER, ERROR_NOT_ENOUGH_MEMORY, ERROR_PATH_NOT_FOUND,
HANDLE, MAX_PATH,
};
use windows_sys::Win32::Storage::FileSystem::{
FILE_NAME_OPENED, GetFinalPathNameByHandleW,
GetFinalPathNameByHandleW, FILE_NAME_OPENED,
};

let handle = std::io::stdout().lock().as_raw_handle() as HANDLE;
Expand Down Expand Up @@ -833,14 +912,21 @@
use filetime::FileTime;

use crate::{
ChangeTimes, Options, Source, determine_atime_mtime_change, error::TouchError, touch,
uu_app,
determine_atime_mtime_change, error::TouchError, touch, uu_app, ChangeTimes, Options,
Source,
};

#[cfg(unix)]
use std::io::ErrorKind;
#[cfg(unix)]
use tempfile::tempdir;

#[cfg(windows)]
use std::env;
#[cfg(windows)]
use uucore::locale;
#[cfg(unix)]
use uucore::translate;

#[cfg(windows)]
#[test]
Expand All @@ -851,12 +937,10 @@
let _ = locale::setup_localization("touch");
// We can trigger an error by not setting stdout to anything (will
// fail with code 1)
assert!(
super::pathbuf_from_stdout()
.expect_err("pathbuf_from_stdout should have failed")
.to_string()
.contains("GetFinalPathNameByHandleW failed with code 1")
);
assert!(super::pathbuf_from_stdout()
.expect_err("pathbuf_from_stdout should have failed")
.to_string()
.contains("GetFinalPathNameByHandleW failed with code 1"));
}

#[test]
Expand Down Expand Up @@ -908,4 +992,63 @@
Ok(_) => panic!("Expected to error with TouchError::InvalidFiletime but succeeded"),
}
}

#[cfg(unix)]
#[test]
fn test_try_futimens_via_write_fd_sets_times() {
let dir = tempdir().unwrap();
let path = dir.path().join("futimens-file");
std::fs::write(&path, b"data").unwrap();

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();

let metadata = std::fs::metadata(&path).unwrap();
let actual_atime = FileTime::from_last_access_time(&metadata);
let actual_mtime = FileTime::from_last_modification_time(&metadata);

assert_eq!(actual_atime, atime);
assert_eq!(actual_mtime, mtime);
}

#[cfg(unix)]
#[test]
fn test_try_futimens_via_write_fd_rejects_non_file() {
let dir = tempdir().unwrap();
let atime = FileTime::from_unix_time(1_600_000_000, 0);
let mtime = FileTime::from_unix_time(1_600_000_001, 0);

let err = super::try_futimens_via_write_fd(dir.path(), atime, mtime)
.expect_err("expected error for non-regular file");
assert_eq!(err.kind(), ErrorKind::Other);
assert!(err
.to_string()
.contains(&translate!("touch-error-not-a-regular-file")));
}

#[cfg(unix)]
#[test]
fn test_update_times_keeps_futimens_error_when_fallback_fails() {
let dir = tempdir().unwrap();
let path = dir.path().join("missing");
let atime = FileTime::from_unix_time(1_600_000_000, 0);
let mtime = FileTime::from_unix_time(1_600_000_001, 0);
let opts = Options {
no_create: false,
no_deref: false,
source: Source::Now,
date: None,
change_times: ChangeTimes::Both,
strict: false,
};

let err = super::update_times(&path, false, &opts, atime, mtime)
.expect_err("expected both futimens and fallback to fail");
let message = err.to_string();

assert!(message.contains("futimens failed:"));
assert!(message.contains("set_file_times fallback failed:"));
}
}
Loading