Repository: uutils/coreutils (language: rust)

Issue:
touch: does not support obsolete POSIX arguments for setting time
Steps to reproduce:
```
_POSIX2_VERSION=199209 POSIXLY_CORRECT=1 touch 01010000 11111111
```

What happens now: uutils `touch` creates two files with the creation time set to now:
```
$ ls -l
-rw-rw-r-- 1 jeffrey jeffrey      0 Jan 20 09:47 01010000
-rw-rw-r-- 1 jeffrey jeffrey      0 Jan 20 09:47 11111111
```

What I expected to happen: GNU `touch` uses the first argument as the creation time and the second argument as the file name:
```
$ ls -l
-rw-rw-r-- 1 jeffrey jeffrey      0 Jan  1 00:00 11111111
```

Notes: this is causing a failure in the GNU test file `tests/touch/obsolescent.sh`. The environment variables are necessary, as mentioned in the GNU documentation for this obsolete feature:

> On systems predating POSIX 1003.1-2001, touch supports an obsolete syntax, as follows. If no timestamp is given with any of the -d, -r, or -t options, and if there are two or more files and the first file is of the form ‘mmddhhmm[yy]’ and this would be a valid argument to the -t option (if the yy, if any, were moved to the front), and if the represented year is in the range 1969–1999, that argument is interpreted as the time for the other files instead of as a file name. Although this obsolete behavior can be controlled with the _POSIX2_VERSION environment variable (see [Standards conformance](https://www.gnu.org/software/coreutils/manual/html_node/Standards-conformance.html)), portable scripts should avoid commands whose behavior depends on this variable. For example, use ‘touch ./12312359 main.c’ or ‘touch -t 12312359 main.c’ rather than the ambiguous ‘touch 12312359 main.c’.

-- https://www.gnu.org/software/coreutils/manual/html_node/touch-invocation.html


Discussion / hints:

Fix patch:
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<DateTime<Local>> {
     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<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() && 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<InputFile> = matches
-        .get_many::<OsString>(ARG_FILES)
+    let mut filenames: Vec<&String> = matches
+        .get_many::<String>(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::<OsString>(options::sources::REFERENCE);
-    let timestamp = matches.get_one::<String>(options::sources::TIMESTAMP);
+    let date = matches
+        .get_one::<String>(options::sources::DATE)
+        .map(|date| date.to_owned());
+
+    let mut timestamp = matches
+        .get_one::<String>(options::sources::TIMESTAMP)
+        .map(|t| t.to_owned());
+
+    if is_first_filename_timestamp(reference, date.as_deref(), &timestamp, &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::<String>(options::sources::DATE)
-        .map(|date| date.to_owned());
+    let files: Vec<InputFile> = 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(


Test patch:
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"));
+}