diff --git a/src/uu/touch/src/touch.rs b/src/uu/touch/src/touch.rs index de66e52ee..047313e64 100644 --- a/src/uu/touch/src/touch.rs +++ b/src/uu/touch/src/touch.rs @@ -135,12 +135,62 @@ 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. +/// +/// `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 { + let bytes = s.as_bytes(); + let n = bytes.len(); + let y1 = bytes[n - 2] - b'0'; + let y2 = bytes[n - 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, + files: &[&String], +) -> bool { + match std::env::var("_POSIX2_VERSION") { + Ok(s) if s == "199209" => { + 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 + } + } + _ => false, + } +} + +/// 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)?; - 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,6 +200,38 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { ), ) })? + .collect(); + + let no_deref = matches.get_flag(options::NO_DEREF); + + let reference = matches.get_one::(options::sources::REFERENCE); + let date = matches + .get_one::(options::sources::DATE) + .map(|date| date.to_owned()); + + let mut timestamp = matches + .get_one::(options::sources::TIMESTAMP) + .map(|t| t.to_owned()); + + 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)?) + } else { + Source::Now + }; + + let files: Vec = filenames + .into_iter() .map(|filename| { if filename == "-" { InputFile::Stdout @@ -159,23 +241,6 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { }) .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 source = if let Some(reference) = reference { - Source::Reference(PathBuf::from(reference)) - } else if let Some(ts) = timestamp { - Source::Timestamp(parse_timestamp(ts)?) - } else { - Source::Now - }; - - let date = matches - .get_one::(options::sources::DATE) - .map(|date| date.to_owned()); - let opts = Options { no_create: matches.get_flag(options::NO_CREATE), no_deref, @@ -275,7 +340,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..ec32aa7b6 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(&["0101000090", "11111111"]) + .succeeds() + .no_output(); + assert!(at.file_exists("11111111")); + assert!(!at.file_exists("0101000090")); +}