diff --git a/src/uu/touch/src/touch.rs b/src/uu/touch/src/touch.rs index de66e52ee28..047313e6487 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,31 +200,46 @@ 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) + .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)?) + Source::Timestamp(parse_timestamp(&ts)?) } else { 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 +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 a0d51c208bb..ec32aa7b6ae 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")); +}