Repository: uutils/coreutils (Rust)

Issue:
touch: `-t` YYMMDDhhmm leads to incorrect year if YY > 68
If a two-digit year is specified with `-t`, GNU `touch` uses the `19`-prefix if the year is greater than `68`, whereas uutils `touch` uses `20`.
```bash
$ touch -t 6801010000 a
$ ls -l a
-rw-r--r-- 1 dho dho 0 Jan  1  2068 a
$ touch -t 6901010000 a
$ ls -l a
-rw-r--r-- 1 dho dho 0 Jan  1  1969 a


$ cargo run -q touch -t 6801010000 a
$ ls -l a
-rw-r--r-- 1 dho dho 0 Jan  1  2068 a
$ cargo run -q touch -t 6901010000 a
$ ls -l a
-rw-r--r-- 1 dho dho 0 Jan  1  2069 a

``` 


Discussion:
That's a very specific issue there 🧐 

I'm curious of why did GNU decided to change the behavior between 68-69 specifically
I don't know what the reason is but the behavior is [documented](https://www.gnu.org/software/coreutils/manual/html_node/touch-invocation.html#index-leap-seconds):

> If the year is specified with only two digits, then cc is 20 for years in the range 0 … 68, and 19 for years in 69 … 99.

My guess is that it is related to its obsolete syntax:

> [...] 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.
I've attempted a fix in #7378 



Patch:
diff --git a/src/uu/touch/src/touch.rs b/src/uu/touch/src/touch.rs
index 047313e6487..eb6154f2be2 100644
--- a/src/uu/touch/src/touch.rs
+++ b/src/uu/touch/src/touch.rs
@@ -640,6 +640,30 @@ fn parse_date(ref_time: DateTime<Local>, s: &str) -> Result<FileTime, TouchError
     Err(TouchError::InvalidDateFormat(s.to_owned()))
 }
 
+/// Prepends 19 or 20 to the year if it is a 2 digit year
+///
+/// GNU `touch` behavior:
+///
+/// - 68 and before is interpreted as 20xx
+/// - 69 and after is interpreted as 19xx
+fn prepend_century(s: &str) -> UResult<String> {
+    let first_two_digits = s[..2]
+        .parse::<u32>()
+        .map_err(|_| USimpleError::new(1, format!("invalid date ts format {}", s.quote())))?;
+    Ok(format!(
+        "{}{s}",
+        if first_two_digits > 68 { 19 } else { 20 }
+    ))
+}
+
+/// Parses a timestamp string into a FileTime.
+///
+/// This function attempts to parse a string into a FileTime
+/// As expected by gnu touch -t : `[[cc]yy]mmddhhmm[.ss]`
+///
+/// Note that  If the year is specified with only two digits,
+/// then cc is 20 for years in the range 0 … 68, and 19 for years in 69 … 99.
+/// in order to be compatible with GNU `touch`.
 fn parse_timestamp(s: &str) -> UResult<FileTime> {
     use format::*;
 
@@ -648,9 +672,9 @@ fn parse_timestamp(s: &str) -> UResult<FileTime> {
     let (format, ts) = match s.chars().count() {
         15 => (YYYYMMDDHHMM_DOT_SS, s.to_owned()),
         12 => (YYYYMMDDHHMM, s.to_owned()),
-        // If we don't add "20", we have insufficient information to parse
-        13 => (YYYYMMDDHHMM_DOT_SS, format!("20{s}")),
-        10 => (YYYYMMDDHHMM, format!("20{s}")),
+        // If we don't add "19" or "20", we have insufficient information to parse
+        13 => (YYYYMMDDHHMM_DOT_SS, prepend_century(s)?),
+        10 => (YYYYMMDDHHMM, prepend_century(s)?),
         11 => (YYYYMMDDHHMM_DOT_SS, format!("{}{}", current_year(), s)),
         8 => (YYYYMMDDHHMM, format!("{}{}", current_year(), s)),
         _ => {


Test patch:
diff --git a/tests/by-util/test_touch.rs b/tests/by-util/test_touch.rs
index 91298ff9e74..c4b27c8b7ab 100644
--- a/tests/by-util/test_touch.rs
+++ b/tests/by-util/test_touch.rs
@@ -118,6 +118,69 @@ fn test_touch_set_mdhms_time() {
     assert_eq!(mtime.unix_seconds() - start_of_year.unix_seconds(), 45296);
 }
 
+#[test]
+#[cfg(target_pointer_width = "64")]
+fn test_touch_2_digit_years_68() {
+    // 68 and before are 20xx
+    // it will fail on 32 bits, because of wraparound for anything after
+    // 2038-01-19
+    let (at, mut ucmd) = at_and_ucmd!();
+    let file = "test_touch_set_two_digit_68_time";
+
+    ucmd.args(&["-t", "6801010000", file])
+        .succeeds()
+        .no_output();
+
+    assert!(at.file_exists(file));
+
+    //  January 1, 2068, 00:00:00
+    let expected = FileTime::from_unix_time(3_092_601_600, 0);
+    let (atime, mtime) = get_file_times(&at, file);
+    assert_eq!(atime, mtime);
+    assert_eq!(atime, expected);
+    assert_eq!(mtime, expected);
+}
+
+#[test]
+fn test_touch_2_digit_years_2038() {
+    // Same as test_touch_2_digit_years_68 but for 32 bits systems
+    // we test a date before the y2038 bug
+    let (at, mut ucmd) = at_and_ucmd!();
+    let file = "test_touch_set_two_digit_68_time";
+
+    ucmd.args(&["-t", "3801010000", file])
+        .succeeds()
+        .no_output();
+
+    assert!(at.file_exists(file));
+
+    // January 1, 2038, 00:00:00
+    let expected = FileTime::from_unix_time(2_145_916_800, 0);
+    let (atime, mtime) = get_file_times(&at, file);
+    assert_eq!(atime, mtime);
+    assert_eq!(atime, expected);
+    assert_eq!(mtime, expected);
+}
+
+#[test]
+fn test_touch_2_digit_years_69() {
+    // 69 and after are 19xx
+    let (at, mut ucmd) = at_and_ucmd!();
+    let file = "test_touch_set_two_digit_69_time";
+
+    ucmd.args(&["-t", "6901010000", file])
+        .succeeds()
+        .no_output();
+
+    assert!(at.file_exists(file));
+    // January 1, 1969, 00:00:00
+    let expected = FileTime::from_unix_time(-31_536_000, 0);
+    let (atime, mtime) = get_file_times(&at, file);
+    assert_eq!(atime, mtime);
+    assert_eq!(atime, expected);
+    assert_eq!(mtime, expected);
+}
+
 #[test]
 fn test_touch_set_ymdhm_time() {
     let (at, mut ucmd) = at_and_ucmd!();