1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-28 19:47:45 +00:00

Fix touch -t with 2 digit years when YY > 68

When using `touch -t` with a 2 digit year, the year is interpreted as
a relative year to 2000.

When the year is 68 or less, it should be interpreted as 20xx.
When the year is 69 or more, it should be interpreted as 19xx.

This is the behavior of GNU `touch`.

fixes gh-7280

Arguably 2 digits years should be deprecated as we
are already closer to 2069, than 1969.
This commit is contained in:
M Bussonnier 2025-03-03 16:52:53 +01:00 committed by Dorian Péron
parent d570512bdc
commit 7632acfc90
2 changed files with 66 additions and 3 deletions

View file

@ -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)),
_ => {

View file

@ -118,6 +118,45 @@ fn test_touch_set_mdhms_time() {
assert_eq!(mtime.unix_seconds() - start_of_year.unix_seconds(), 45296);
}
#[test]
fn test_touch_2_digit_years_68() {
// 68 and before are 20xx
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_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!();