From 6dfa1f827615ecb637f284797def5829c93263de Mon Sep 17 00:00:00 2001 From: Jeffrey Finkelstein Date: Mon, 3 Feb 2025 20:24:45 -0500 Subject: [PATCH 1/3] 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 de66e52ee28..323d7a11d4a 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 a0d51c208bb..194ac0e7b22 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")); +} From c23e1db9c545f176d550ea1d6a381c8d8f6bdde2 Mon Sep 17 00:00:00 2001 From: Jeffrey Finkelstein Date: Thu, 6 Feb 2025 22:02:47 -0500 Subject: [PATCH 2/3] Use the last, not first, two digits as the year --- src/uu/touch/src/touch.rs | 41 +++++++++++++++++++++++++++---------- tests/by-util/test_touch.rs | 4 ++-- 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/src/uu/touch/src/touch.rs b/src/uu/touch/src/touch.rs index 323d7a11d4a..2365d5c83e6 100644 --- a/src/uu/touch/src/touch.rs +++ b/src/uu/touch/src/touch.rs @@ -141,11 +141,14 @@ fn all_digits(s: &str) -> bool { } /// Convert a two-digit year string to the corresponding number. +/// +/// `s` must be of length two or more. The last two bytes of `s` are +/// assumed to be the two digits of the year. 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'; + let n = bytes.len(); + let y1 = bytes[n - 2] - b'0'; + let y2 = bytes[n - 1] - b'0'; 10 * y1 + y2 } @@ -153,7 +156,7 @@ fn get_year(s: &str) -> u8 { fn is_first_filename_timestamp( reference: Option<&OsString>, date: Option<&str>, - timestamp: Option<&String>, + timestamp: &Option, files: &[&String], ) -> bool { match std::env::var("_POSIX2_VERSION") { @@ -180,6 +183,18 @@ fn is_first_filename_timestamp( } } +/// Cycle the last two characters to the beginning of the string. +/// +/// `s` must have length at least two. +fn shr2(s: &str) -> String { + let n = s.len(); + let (a, b) = s.split_at(n - 2); + let mut result = String::with_capacity(n); + result.push_str(b); + result.push_str(a); + result +} + #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uu_app().try_get_matches_from(args)?; @@ -204,19 +219,23 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { .get_one::(options::sources::DATE) .map(|date| date.to_owned()); - let mut timestamp = matches.get_one::(options::sources::TIMESTAMP); + let mut timestamp = matches + .get_one::(options::sources::TIMESTAMP) + .map(|t| t.to_owned()); - 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(); + if is_first_filename_timestamp(reference, date.as_deref(), ×tamp, &filenames) { + timestamp = if filenames[0].len() == 10 { + Some(shr2(filenames[0])) + } else { + Some(filenames[0].to_string()) + }; + filenames = filenames[1..].to_vec(); } let source = if let Some(reference) = reference { Source::Reference(PathBuf::from(reference)) } else if let Some(ts) = timestamp { - Source::Timestamp(parse_timestamp(ts)?) + Source::Timestamp(parse_timestamp(&ts)?) } else { Source::Now }; diff --git a/tests/by-util/test_touch.rs b/tests/by-util/test_touch.rs index 194ac0e7b22..ec32aa7b6ae 100644 --- a/tests/by-util/test_touch.rs +++ b/tests/by-util/test_touch.rs @@ -935,9 +935,9 @@ 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"]) + .args(&["0101000090", "11111111"]) .succeeds() .no_output(); assert!(at.file_exists("11111111")); - assert!(!at.file_exists("01010000")); + assert!(!at.file_exists("0101000090")); } From 864215653e28b15500857a730fabfa7a7b705cad Mon Sep 17 00:00:00 2001 From: Jeffrey Finkelstein Date: Sat, 8 Feb 2025 08:49:08 -0500 Subject: [PATCH 3/3] Collapse multiple if statements with && --- src/uu/touch/src/touch.rs | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/src/uu/touch/src/touch.rs b/src/uu/touch/src/touch.rs index 2365d5c83e6..047313e6487 100644 --- a/src/uu/touch/src/touch.rs +++ b/src/uu/touch/src/touch.rs @@ -161,20 +161,10 @@ fn is_first_filename_timestamp( ) -> 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 - } + if timestamp.is_none() && reference.is_none() && date.is_none() && files.len() >= 2 { + let s = files[0]; + all_digits(s) + && (s.len() == 8 || (s.len() == 10 && (69..=99).contains(&get_year(s)))) } else { false }