From 6dfa1f827615ecb637f284797def5829c93263de Mon Sep 17 00:00:00 2001 From: Jeffrey Finkelstein Date: Mon, 3 Feb 2025 20:24:45 -0500 Subject: [PATCH] touch: support obsolete POSIX timestamp argument Support obsolete form of timestamp argument for old POSIX versions. In summary, when older versions of POSIX are used and the first positional argument looks like a date and time, then treat it as a timestamp instead of as a filename. For example, before this commit _POSIX2_VERSION=199209 POSIXLY_CORRECT=1 touch 01010000 11111111 would create two files, `01010000` and `11111111`. After this commit, the first argument is interpreted as a date and time (in this case, midnight on January 1 of the current year) and that date and time are set on the file named `11111111`. Fixes #7180. --- src/uu/touch/src/touch.rs | 83 ++++++++++++++++++++++++++++++------- tests/by-util/test_touch.rs | 24 +++++++++++ 2 files changed, 93 insertions(+), 14 deletions(-) diff --git a/src/uu/touch/src/touch.rs b/src/uu/touch/src/touch.rs index de66e52ee..323d7a11d 100644 --- a/src/uu/touch/src/touch.rs +++ b/src/uu/touch/src/touch.rs @@ -135,12 +135,57 @@ fn filetime_to_datetime(ft: &FileTime) -> Option> { Some(DateTime::from_timestamp(ft.unix_seconds(), ft.nanoseconds())?.into()) } +/// Whether all characters in the string are digits. +fn all_digits(s: &str) -> bool { + s.as_bytes().iter().all(u8::is_ascii_digit) +} + +/// Convert a two-digit year string to the corresponding number. +fn get_year(s: &str) -> u8 { + // Pre-condition: s.len() >= 2 + let bytes = s.as_bytes(); + let y1 = bytes[0] - b'0'; + let y2 = bytes[1] - b'0'; + 10 * y1 + y2 +} + +/// Whether the first filename should be interpreted as a timestamp. +fn is_first_filename_timestamp( + reference: Option<&OsString>, + date: Option<&str>, + timestamp: Option<&String>, + files: &[&String], +) -> bool { + match std::env::var("_POSIX2_VERSION") { + Ok(s) if s == "199209" => { + if timestamp.is_none() && reference.is_none() && date.is_none() { + if files.len() >= 2 { + let s = files[0]; + if s.len() == 8 && all_digits(s) { + true + } else if s.len() == 10 && all_digits(s) { + let year = get_year(s); + (69..=99).contains(&year) + } else { + false + } + } else { + false + } + } else { + false + } + } + _ => false, + } +} + #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uu_app().try_get_matches_from(args)?; - let files: Vec = matches - .get_many::(ARG_FILES) + let mut filenames: Vec<&String> = matches + .get_many::(ARG_FILES) .ok_or_else(|| { USimpleError::new( 1, @@ -150,19 +195,23 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { ), ) })? - .map(|filename| { - if filename == "-" { - InputFile::Stdout - } else { - InputFile::Path(PathBuf::from(filename)) - } - }) .collect(); let no_deref = matches.get_flag(options::NO_DEREF); let reference = matches.get_one::(options::sources::REFERENCE); - let timestamp = matches.get_one::(options::sources::TIMESTAMP); + let date = matches + .get_one::(options::sources::DATE) + .map(|date| date.to_owned()); + + let mut timestamp = matches.get_one::(options::sources::TIMESTAMP); + + if is_first_filename_timestamp(reference, date.as_deref(), timestamp, &filenames) { + let head = filenames[0]; + let tail = &filenames[1..]; + timestamp = Some(head); + filenames = tail.to_vec(); + } let source = if let Some(reference) = reference { Source::Reference(PathBuf::from(reference)) @@ -172,9 +221,16 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { Source::Now }; - let date = matches - .get_one::(options::sources::DATE) - .map(|date| date.to_owned()); + let files: Vec = filenames + .into_iter() + .map(|filename| { + if filename == "-" { + InputFile::Stdout + } else { + InputFile::Path(PathBuf::from(filename)) + } + }) + .collect(); let opts = Options { no_create: matches.get_flag(options::NO_CREATE), @@ -275,7 +331,6 @@ pub fn uu_app() -> Command { Arg::new(ARG_FILES) .action(ArgAction::Append) .num_args(1..) - .value_parser(ValueParser::os_string()) .value_hint(clap::ValueHint::AnyPath), ) .group( diff --git a/tests/by-util/test_touch.rs b/tests/by-util/test_touch.rs index a0d51c208..194ac0e7b 100644 --- a/tests/by-util/test_touch.rs +++ b/tests/by-util/test_touch.rs @@ -917,3 +917,27 @@ fn test_touch_reference_symlink_with_no_deref() { // Times should be taken from the symlink, not the destination assert_eq!((time, time), get_symlink_times(&at, arg)); } + +#[test] +fn test_obsolete_posix_format() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.env("_POSIX2_VERSION", "199209") + .env("POSIXLY_CORRECT", "1") + .args(&["01010000", "11111111"]) + .succeeds() + .no_output(); + assert!(at.file_exists("11111111")); + assert!(!at.file_exists("01010000")); +} + +#[test] +fn test_obsolete_posix_format_with_year() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.env("_POSIX2_VERSION", "199209") + .env("POSIXLY_CORRECT", "1") + .args(&["9001010000", "11111111"]) + .succeeds() + .no_output(); + assert!(at.file_exists("11111111")); + assert!(!at.file_exists("01010000")); +}