use chrono::{DateTime, Datelike, Duration, Local, NaiveDate, NaiveTime, TimeZone, Timelike, Utc};
use clap::builder::ValueParser;
use clap::{crate_version, Arg, ArgAction, ArgGroup, Command};
use filetime::{set_file_times, set_symlink_file_times, FileTime};
use std::ffi::OsString;
use std::fs::{self, File};
use std::path::{Path, PathBuf};
use uucore::display::Quotable;
use uucore::error::{FromIo, UError, UResult, USimpleError};
use uucore::{format_usage, help_about, help_usage, show};
const ABOUT: &str = help_about!("touch.md");
const USAGE: &str = help_usage!("touch.md");
pub mod options {
pub static SOURCES: &str = "sources";
pub mod sources {
pub static DATE: &str = "date";
pub static REFERENCE: &str = "reference";
pub static TIMESTAMP: &str = "timestamp";
}
pub static HELP: &str = "help";
pub static ACCESS: &str = "access";
pub static MODIFICATION: &str = "modification";
pub static NO_CREATE: &str = "no-create";
pub static NO_DEREF: &str = "no-dereference";
pub static TIME: &str = "time";
}
static ARG_FILES: &str = "files";
mod format {
pub(crate) const POSIX_LOCALE: &str = "%a %b %e %H:%M:%S %Y";
pub(crate) const ISO_8601: &str = "%Y-%m-%d";
pub(crate) const YYYYMMDDHHMM_DOT_SS: &str = "%Y%m%d%H%M.%S";
pub(crate) const YYYYMMDDHHMMSS: &str = "%Y-%m-%d %H:%M:%S.%f";
pub(crate) const YYYYMMDDHHMMS: &str = "%Y-%m-%d %H:%M:%S";
pub(crate) const YYYY_MM_DD_HH_MM: &str = "%Y-%m-%d %H:%M";
pub(crate) const YYYYMMDDHHMM: &str = "%Y%m%d%H%M";
pub(crate) const YYYYMMDDHHMM_OFFSET: &str = "%Y-%m-%d %H:%M %z";
}
fn datetime_to_filetime<T: TimeZone>(dt: &DateTime<T>) -> FileTime {
FileTime::from_unix_time(dt.timestamp(), dt.timestamp_subsec_nanos())
}
#[uucore::main]
#[allow(clippy::cognitive_complexity)]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let matches = uu_app().try_get_matches_from(args)?;
let files = matches.get_many::<OsString>(ARG_FILES).ok_or_else(|| {
USimpleError::new(
1,
format!(
"missing file operand\nTry '{} --help' for more information.",
uucore::execution_phrase()
),
)
})?;
let (mut atime, mut mtime) = match (
matches.get_one::<OsString>(options::sources::REFERENCE),
matches.get_one::<String>(options::sources::DATE),
) {
(Some(reference), Some(date)) => {
let (atime, mtime) = stat(Path::new(reference), !matches.get_flag(options::NO_DEREF))?;
if let Ok(offset) = parse_datetime::from_str(date) {
let seconds = offset.num_seconds();
let nanos = offset.num_nanoseconds().unwrap_or(0) % 1_000_000_000;
let ref_atime_secs = atime.unix_seconds();
let ref_atime_nanos = atime.nanoseconds();
let atime = FileTime::from_unix_time(
ref_atime_secs + seconds,
ref_atime_nanos + nanos as u32,
);
let ref_mtime_secs = mtime.unix_seconds();
let ref_mtime_nanos = mtime.nanoseconds();
let mtime = FileTime::from_unix_time(
ref_mtime_secs + seconds,
ref_mtime_nanos + nanos as u32,
);
(atime, mtime)
} else {
let timestamp = parse_date(date)?;
(timestamp, timestamp)
}
}
(Some(reference), None) => {
stat(Path::new(reference), !matches.get_flag(options::NO_DEREF))?
}
(None, Some(date)) => {
let timestamp = parse_date(date)?;
(timestamp, timestamp)
}
(None, None) => {
let timestamp = if let Some(ts) = matches.get_one::<String>(options::sources::TIMESTAMP)
{
parse_timestamp(ts)?
} else {
datetime_to_filetime(&Local::now())
};
(timestamp, timestamp)
}
};
for filename in files {
let pathbuf = if filename == "-" {
pathbuf_from_stdout()?
} else {
PathBuf::from(filename)
};
let path = pathbuf.as_path();
let metadata_result = if matches.get_flag(options::NO_DEREF) {
path.symlink_metadata()
} else {
path.metadata()
};
if let Err(e) = metadata_result {
if e.kind() != std::io::ErrorKind::NotFound {
return Err(e.map_err_context(|| format!("setting times of {}", filename.quote())));
}
if matches.get_flag(options::NO_CREATE) {
continue;
}
if matches.get_flag(options::NO_DEREF) {
show!(USimpleError::new(
1,
format!(
"setting times of {}: No such file or directory",
filename.quote()
)
));
continue;
}
if let Err(e) = File::create(path) {
show!(e.map_err_context(|| format!("cannot touch {}", path.quote())));
continue;
};
if !matches.contains_id(options::SOURCES) {
continue;
}
}
if matches.get_flag(options::ACCESS)
|| matches.get_flag(options::MODIFICATION)
|| matches.contains_id(options::TIME)
{
let st = stat(path, !matches.get_flag(options::NO_DEREF))?;
let time = matches
.get_one::<String>(options::TIME)
.map(|s| s.as_str())
.unwrap_or("");
if !(matches.get_flag(options::ACCESS)
|| time.contains(&"access".to_owned())
|| time.contains(&"atime".to_owned())
|| time.contains(&"use".to_owned()))
{
atime = st.0;
}
if !(matches.get_flag(options::MODIFICATION)
|| time.contains(&"modify".to_owned())
|| time.contains(&"mtime".to_owned()))
{
mtime = st.1;
}
}
if filename == "-" {
filetime::set_file_times(path, atime, mtime)
} else if matches.get_flag(options::NO_DEREF) {
set_symlink_file_times(path, atime, mtime)
} else {
set_file_times(path, atime, mtime)
}
.map_err_context(|| format!("setting times of {}", path.quote()))?;
}
Ok(())
}
pub fn uu_app() -> Command {
Command::new(uucore::util_name())
.version(crate_version!())
.about(ABOUT)
.override_usage(format_usage(USAGE))
.infer_long_args(true)
.disable_help_flag(true)
.arg(
Arg::new(options::HELP)
.long(options::HELP)
.help("Print help information.")
.action(ArgAction::Help),
)
.arg(
Arg::new(options::ACCESS)
.short('a')
.help("change only the access time")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::sources::TIMESTAMP)
.short('t')
.help("use [[CC]YY]MMDDhhmm[.ss] instead of the current time")
.value_name("STAMP"),
)
.arg(
Arg::new(options::sources::DATE)
.short('d')
.long(options::sources::DATE)
.allow_hyphen_values(true)
.help("parse argument and use it instead of current time")
.value_name("STRING")
.conflicts_with(options::sources::TIMESTAMP),
)
.arg(
Arg::new(options::MODIFICATION)
.short('m')
.help("change only the modification time")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::NO_CREATE)
.short('c')
.long(options::NO_CREATE)
.help("do not create any files")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::NO_DEREF)
.short('h')
.long(options::NO_DEREF)
.help(
"affect each symbolic link instead of any referenced file \
(only for systems that can change the timestamps of a symlink)",
)
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::sources::REFERENCE)
.short('r')
.long(options::sources::REFERENCE)
.help("use this file's times instead of the current time")
.value_name("FILE")
.value_parser(ValueParser::os_string())
.value_hint(clap::ValueHint::AnyPath)
.conflicts_with(options::sources::TIMESTAMP),
)
.arg(
Arg::new(options::TIME)
.long(options::TIME)
.help(
"change only the specified time: \"access\", \"atime\", or \
\"use\" are equivalent to -a; \"modify\" or \"mtime\" are \
equivalent to -m",
)
.value_name("WORD")
.value_parser(["access", "atime", "use", "modify", "mtime"]),
)
.arg(
Arg::new(ARG_FILES)
.action(ArgAction::Append)
.num_args(1..)
.value_parser(ValueParser::os_string())
.value_hint(clap::ValueHint::AnyPath),
)
.group(
ArgGroup::new(options::SOURCES)
.args([
options::sources::TIMESTAMP,
options::sources::DATE,
options::sources::REFERENCE,
])
.multiple(true),
)
}
fn stat(path: &Path, follow: bool) -> UResult<(FileTime, FileTime)> {
let metadata = match fs::metadata(path) {
Ok(metadata) => metadata,
Err(e) if e.kind() == std::io::ErrorKind::NotFound && !follow => fs::symlink_metadata(path)
.map_err_context(|| format!("failed to get attributes of {}", path.quote()))?,
Err(e) => return Err(e.into()),
};
Ok((
FileTime::from_last_access_time(&metadata),
FileTime::from_last_modification_time(&metadata),
))
}
fn parse_date(s: &str) -> UResult<FileTime> {
if let Ok(parsed) = Local.datetime_from_str(s, format::POSIX_LOCALE) {
return Ok(datetime_to_filetime(&parsed));
}
for fmt in [
format::YYYYMMDDHHMMS,
format::YYYYMMDDHHMMSS,
format::YYYY_MM_DD_HH_MM,
format::YYYYMMDDHHMM_OFFSET,
] {
if let Ok(parsed) = Utc.datetime_from_str(s, fmt) {
return Ok(datetime_to_filetime(&parsed));
}
}
if let Ok(parsed_date) = NaiveDate::parse_from_str(s, format::ISO_8601) {
let parsed = Local
.from_local_datetime(&parsed_date.and_time(NaiveTime::MIN))
.unwrap();
return Ok(datetime_to_filetime(&parsed));
}
if s.bytes().next() == Some(b'@') {
if let Ok(ts) = &s[1..].parse::<i64>() {
return Ok(FileTime::from_unix_time(*ts, 0));
}
}
if let Ok(duration) = parse_datetime::from_str(s) {
let dt = Local::now() + duration;
return Ok(datetime_to_filetime(&dt));
}
Err(USimpleError::new(1, format!("Unable to parse date: {s}")))
}
fn parse_timestamp(s: &str) -> UResult<FileTime> {
use format::*;
let current_year = || Local::now().year();
let (format, ts) = match s.chars().count() {
15 => (YYYYMMDDHHMM_DOT_SS, s.to_owned()),
12 => (YYYYMMDDHHMM, s.to_owned()),
13 => (YYYYMMDDHHMM_DOT_SS, format!("20{}", s)),
10 => (YYYYMMDDHHMM, format!("20{}", s)),
11 => (YYYYMMDDHHMM_DOT_SS, format!("{}{}", current_year(), s)),
8 => (YYYYMMDDHHMM, format!("{}{}", current_year(), s)),
_ => {
return Err(USimpleError::new(
1,
format!("invalid date format {}", s.quote()),
))
}
};
let mut local = chrono::Local
.datetime_from_str(&ts, format)
.map_err(|_| USimpleError::new(1, format!("invalid date ts format {}", ts.quote())))?;
if local.second() == 59 && ts.ends_with(".60") {
local += Duration::seconds(1);
}
let local2 = local + Duration::hours(1) - Duration::hours(1);
if local.hour() != local2.hour() {
return Err(USimpleError::new(
1,
format!("invalid date format {}", s.quote()),
));
}
Ok(datetime_to_filetime(&local))
}
fn pathbuf_from_stdout() -> UResult<PathBuf> {
#[cfg(all(unix, not(target_os = "android")))]
{
Ok(PathBuf::from("/dev/stdout"))
}
#[cfg(target_os = "android")]
{
Ok(PathBuf::from("/proc/self/fd/1"))
}
#[cfg(windows)]
{
use std::os::windows::prelude::AsRawHandle;
use windows_sys::Win32::Foundation::{
GetLastError, ERROR_INVALID_PARAMETER, ERROR_NOT_ENOUGH_MEMORY, ERROR_PATH_NOT_FOUND,
HANDLE, MAX_PATH,
};
use windows_sys::Win32::Storage::FileSystem::{
GetFinalPathNameByHandleW, FILE_NAME_OPENED,
};
let handle = std::io::stdout().lock().as_raw_handle() as HANDLE;
let mut file_path_buffer: [u16; MAX_PATH as usize] = [0; MAX_PATH as usize];
let ret = unsafe {
GetFinalPathNameByHandleW(
handle,
file_path_buffer.as_mut_ptr(),
file_path_buffer.len() as u32,
FILE_NAME_OPENED,
)
};
let buffer_size = match ret {
ERROR_PATH_NOT_FOUND | ERROR_NOT_ENOUGH_MEMORY | ERROR_INVALID_PARAMETER => {
return Err(USimpleError::new(
1,
format!("GetFinalPathNameByHandleW failed with code {ret}"),
))
}
e if e == 0 => {
return Err(USimpleError::new(
1,
format!(
"GetFinalPathNameByHandleW failed with code {}",
unsafe { GetLastError() }
),
));
}
e => e as usize,
};
Ok(String::from_utf16(&file_path_buffer[0..buffer_size])
.map_err(|e| USimpleError::new(1, e.to_string()))?
.into())
}
}
#[cfg(test)]
mod tests {
#[cfg(windows)]
#[test]
fn test_get_pathbuf_from_stdout_fails_if_stdout_is_not_a_file() {
assert!(super::pathbuf_from_stdout()
.expect_err("pathbuf_from_stdout should have failed")
.to_string()
.contains("GetFinalPathNameByHandleW failed with code 1"));
}
}