Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 81 additions & 15 deletions src/uu/date/src/date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.

// spell-checker:ignore strtime ; (format) DATEFILE MMDDhhmm ; (vars) datetime datetimes getres AWST ACST AEST foobarbaz
// spell-checker:ignore strtime rsplit ; (format) DATEFILE MMDDhhmm ; (vars) datetime datetimes getres AWST ACST AEST foobarbaz

mod format_modifiers;
mod locale;
Expand Down Expand Up @@ -280,6 +280,60 @@ fn parse_military_timezone_with_offset(s: &str) -> Option<(i32, DayDelta)> {
Some((hours_from_midnight, day_delta))
}

/// Parse a positional argument with format `MMDDhhmm[[CC]YY][.ss]`
fn parse_positional_set_datetime(s: &str, utc: bool) -> Option<Zoned> {
let (base, ss) = if let Some((before, after)) = s.rsplit_once('.') {
if after.len() != 2 || !after.chars().all(|c| c.is_ascii_digit()) {
return None;
}
(before, Some(after))
} else {
(s, None)
};

if !base.chars().all(|c| c.is_ascii_digit()) {
return None;
}

let tz = if utc {
TimeZone::UTC
} else {
TimeZone::system()
};

let rearranged = match base.len() {
8 => {
let year = Timestamp::now().to_zoned(tz.clone()).year();
format!("{year}{base}")
}
10 => {
let yy: u32 = base[8..10].parse().ok()?;
let century = if yy > 68 { 19 } else { 20 };
format!("{century}{}{}", &base[8..], &base[..8])
}
12 => format!("{}{}", &base[8..], &base[..8]),
_ => return None,
};

let with_seconds = if let Some(seconds) = ss {
format!("{rearranged}.{seconds}")
} else {
rearranged
};

let parse_format = if ss.is_none() {
"%Y%m%d%H%M"
} else {
"%Y%m%d%H%M.%S"
};

let dt = strtime::parse(parse_format, &with_seconds)
.and_then(|parsed| parsed.to_datetime())
.ok()?;

tz.to_ambiguous_zoned(dt).unambiguous().ok()
}

#[uucore::main]
#[allow(clippy::cognitive_complexity)]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
Expand Down Expand Up @@ -317,25 +371,34 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
}
}

let utc = matches.get_flag(OPT_UNIVERSAL);

let mut positional_set_to: Option<Zoned> = None;

let format = if let Some(form) = matches.get_one::<String>(OPT_FORMAT) {
if !form.starts_with('+') {
// if an optional Format String was found but the user has not provided an input date
// GNU prints an invalid date Error
if !matches!(date_source, DateSource::Human(_)) {
return Err(USimpleError::new(
1,
translate!("date-error-invalid-date", "date" => form),
));
}
// If the user did provide an input date with the --date flag and the Format String is
// not starting with '+' GNU prints the missing '+' error message
if let Some(stripped) = form.strip_prefix('+') {
Format::Custom(stripped.to_string())
} else if matches!(date_source, DateSource::Human(_)) {
// -d was given, positional must be +FORMAT
return Err(USimpleError::new(
1,
translate!("date-error-format-missing-plus", "arg" => form),
));
} else if matches.get_one::<String>(OPT_SET).is_some() {
// -s flag is present: positional must be +FORMAT
return Err(USimpleError::new(
1,
translate!("date-error-invalid-date", "date" => form),
));
} else if let Some(zoned) = parse_positional_set_datetime(form, utc) {
positional_set_to = Some(zoned);
Format::Default
} else {
return Err(USimpleError::new(
1,
translate!("date-error-invalid-date", "date" => form),
));
}
let form = form[1..].to_string();
Format::Custom(form)
} else if let Some(fmt) = matches
.get_many::<String>(OPT_ISO_8601)
.map(|mut iter| iter.next().unwrap_or(&DATE.to_string()).as_str().into())
Expand All @@ -354,7 +417,6 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
Format::Default
};

let utc = matches.get_flag(OPT_UNIVERSAL);
let debug_mode = matches.get_flag(OPT_DEBUG);

// Get the current time, either in the local time zone or UTC.
Expand All @@ -378,6 +440,10 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
Some(Ok(date)) => Some(date),
};

// positional_set_to and set_to are mutually exclusive:
// MMDDhhmm parsing is only attempted when -s is absent.
let set_to = positional_set_to.or(set_to);

let settings = Settings {
utc,
format,
Expand Down
205 changes: 205 additions & 0 deletions tests/by-util/test_date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2836,3 +2836,208 @@ fn test_korean_time_zone() {
.succeeds()
.stdout_is("2026-01-15 01:00:00 UTC\n");
}

#[test]
fn test_positional_set_invalid_month() {
new_ucmd!()
.arg("13011200")
.fails_with_code(1)
.stderr_contains("invalid date");
}

#[test]
fn test_positional_set_invalid_day() {
new_ucmd!()
.arg("01321200")
.fails_with_code(1)
.stderr_contains("invalid date");
}

#[test]
fn test_positional_set_feb_30() {
new_ucmd!()
.arg("02301200")
.fails_with_code(1)
.stderr_contains("invalid date");
}

#[test]
fn test_positional_invalid_hour() {
new_ucmd!()
.arg("01012500")
.fails_with_code(1)
.stderr_contains("invalid date");
}

#[test]
fn test_positional_set_invalid_minute() {
new_ucmd!()
.arg("01010160")
.fails_with_code(1)
.stderr_contains("invalid date");
}

#[test]
fn test_positional_set_invalid_length() {
new_ucmd!()
.arg("010101600")
.fails_with_code(1)
.stderr_contains("invalid date");
}

#[test]
fn test_positional_set_non_digits() {
new_ucmd!()
.arg("010110a")
.fails_with_code(1)
.stderr_contains("invalid date");
}

#[test]
fn test_positional_set_wrong_seconds_suffix() {
new_ucmd!()
.arg("01011200.1")
.fails_with_code(1)
.stderr_contains("invalid date");
new_ucmd!()
.arg("01012359.aa")
.fails_with_code(1)
.stderr_contains("invalid date");
new_ucmd!()
.arg("01011200.123")
.fails_with_code(1)
.stderr_contains("invalid date");
}

#[test]
fn test_positional_set_with_extra_operand() {
new_ucmd!()
.args(&["01011200", "+%Y"])
.fails_with_code(1)
.stderr_contains("extra operand");
}

#[test]
fn test_positional_set_with_d_flag_format_error() {
new_ucmd!()
.args(&["--date", "2025-01-01", "01012025"])
.fails_with_code(1)
.stderr_contains("lacks a leading '+'");
}

#[test]
#[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))]
fn test_positional_set_12_digits() {
if geteuid() == 0 || uucore::os::is_wsl_1() {
return;
}
let result = new_ucmd!().arg("010112002025").fails();
result.no_stdout();
assert!(
result.stderr_str().starts_with("date: cannot set date: "),
"Expected permission error, got: {}",
result.stderr_str()
);
}

#[test]
#[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))]
fn test_positional_set_8_digits() {
if geteuid() == 0 || uucore::os::is_wsl_1() {
return;
}
let result = new_ucmd!().arg("01011200").fails();
result.no_stdout();
assert!(
result.stderr_str().starts_with("date: cannot set date: "),
"Expected permission error, got: {}",
result.stderr_str()
);
}

#[test]
#[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))]
fn test_positional_set_10_digits() {
if geteuid() == 0 || uucore::os::is_wsl_1() {
return;
}
let result = new_ucmd!().arg("0101120025").fails();
result.no_stdout();
assert!(
result.stderr_str().starts_with("date: cannot set date: "),
"Expected permission error, got: {}",
result.stderr_str()
);
}

#[test]
#[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))]
fn test_positional_set_12_digits_with_seconds() {
if geteuid() == 0 || uucore::os::is_wsl_1() {
return;
}
let result = new_ucmd!().arg("010112002025.40").fails();
result.no_stdout();
assert!(
result.stderr_str().starts_with("date: cannot set date: "),
"Expected permission error, got: {}",
result.stderr_str()
);
}

#[test]
#[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))]
fn test_positional_set_12_digits_with_utc_flag() {
if geteuid() == 0 || uucore::os::is_wsl_1() {
return;
}
let result = new_ucmd!().args(&["-u", "010112002025"]).fails();
result.no_stdout();
assert!(
result.stderr_str().starts_with("date: cannot set date: "),
"Expected permission error, got: {}",
result.stderr_str()
);
}

#[test]
#[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))]
fn test_positional_set_century_rule_68() {
if geteuid() == 0 || uucore::os::is_wsl_1() {
return;
}
let result = new_ucmd!().arg("0101120068").fails();
result.no_stdout();
assert!(
result.stderr_str().starts_with("date: cannot set date: "),
"Expected permission error, got: {}",
result.stderr_str()
);
}

#[test]
#[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))]
fn test_positional_set_century_rule_69() {
if geteuid() == 0 || uucore::os::is_wsl_1() {
return;
}
let result = new_ucmd!().arg("0101120069").fails();
result.no_stdout();
assert!(
result.stderr_str().starts_with("date: cannot set date: "),
"Expected permission error, got: {}",
result.stderr_str()
);
}

#[test]
#[cfg(target_os = "macos")]
fn test_positional_set_macos_unavailable() {
let result = new_ucmd!().arg("010112002025").fails();
result.no_stdout();
assert!(
result
.stderr_str()
.starts_with("date: setting the date is not supported by macOS")
);
}
Loading