From e6aee08bf656b4e9d7383b3c3ad48c07c857f378 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 2 Nov 2025 21:48:49 +0100 Subject: [PATCH 1/5] date: handle the empty arguments --- fuzz/fuzz_targets/fuzz_date.rs | 18 +++++++++++++++--- tests/by-util/test_date.rs | 7 +++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/fuzz/fuzz_targets/fuzz_date.rs b/fuzz/fuzz_targets/fuzz_date.rs index 0f9cb262c06..a52788a6cbe 100644 --- a/fuzz/fuzz_targets/fuzz_date.rs +++ b/fuzz/fuzz_targets/fuzz_date.rs @@ -3,12 +3,24 @@ use libfuzzer_sys::fuzz_target; use std::ffi::OsString; use uu_date::uumain; +use uufuzz::generate_and_run_uumain; fuzz_target!(|data: &[u8]| { let delim: u8 = 0; // Null byte - let args = data + let args: Vec = data .split(|b| *b == delim) .filter_map(|e| std::str::from_utf8(e).ok()) - .map(OsString::from); - uumain(args); + .map(OsString::from) + .collect(); + + // Ensure we have at least a program name + if args.is_empty() { + return; + } + + let date_main = |args: std::vec::IntoIter| -> i32 { + uumain(args) + }; + + let _ = generate_and_run_uumain(&args, date_main, None); }); diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index 372e9193770..750d603d29d 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -17,6 +17,13 @@ fn test_invalid_arg() { new_ucmd!().arg("--definitely-invalid").fails_with_code(1); } +#[test] +fn test_empty_arguments() { + new_ucmd!().arg("").fails_with_code(1); + new_ucmd!().args(&["", ""]).fails_with_code(1); + new_ucmd!().args(&["", "", ""]).fails_with_code(1); +} + #[test] fn test_date_email() { for param in ["--rfc-email", "--rfc-e", "-R", "--rfc-2822", "--rfc-822"] { From 870b037b429869e43f56a234243ad03134bd2c13 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 2 Nov 2025 22:38:05 +0100 Subject: [PATCH 2/5] date: allow extra operand --- src/uu/date/src/date.rs | 13 ++++++++++++- tests/by-util/test_date.rs | 8 ++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/uu/date/src/date.rs b/src/uu/date/src/date.rs index e6e57c6785e..4814c4a59a0 100644 --- a/src/uu/date/src/date.rs +++ b/src/uu/date/src/date.rs @@ -173,6 +173,17 @@ fn parse_military_timezone_with_offset(s: &str) -> Option { pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?; + // Check for extra operands (multiple positional arguments) + if let Some(formats) = matches.get_many::(OPT_FORMAT) { + let format_args: Vec<&String> = formats.collect(); + if format_args.len() > 1 { + return Err(USimpleError::new( + 1, + translate!("date-error-extra-operand", "operand" => format_args[1]), + )); + } + } + let format = if let Some(form) = matches.get_one::(OPT_FORMAT) { if !form.starts_with('+') { return Err(USimpleError::new( @@ -513,7 +524,7 @@ pub fn uu_app() -> Command { .help(translate!("date-help-universal")) .action(ArgAction::SetTrue), ) - .arg(Arg::new(OPT_FORMAT)) + .arg(Arg::new(OPT_FORMAT).num_args(0..)) } /// Return the appropriate format string for the given settings. diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index 750d603d29d..0664af3b0e6 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -24,6 +24,14 @@ fn test_empty_arguments() { new_ucmd!().args(&["", "", ""]).fails_with_code(1); } +#[test] +fn test_extra_operands() { + new_ucmd!() + .args(&["test", "extra"]) + .fails_with_code(1) + .stderr_contains("extra operand 'extra'"); +} + #[test] fn test_date_email() { for param in ["--rfc-email", "--rfc-e", "-R", "--rfc-2822", "--rfc-822"] { From d06780f8cf08856b7bcea3400b3f071db2bb386c Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 2 Nov 2025 23:10:29 +0100 Subject: [PATCH 3/5] date: handle unknown options gracefully --- src/uu/date/locales/en-US.ftl | 1 + src/uu/date/locales/fr-FR.ftl | 1 + src/uu/date/src/date.rs | 25 +++++++++++++++++++++++-- tests/by-util/test_date.rs | 24 ++++++++++++++++++++++++ 4 files changed, 49 insertions(+), 2 deletions(-) diff --git a/src/uu/date/locales/en-US.ftl b/src/uu/date/locales/en-US.ftl index 72113c40567..b320cefefc5 100644 --- a/src/uu/date/locales/en-US.ftl +++ b/src/uu/date/locales/en-US.ftl @@ -104,3 +104,4 @@ date-error-date-overflow = date overflow '{$date}' date-error-setting-date-not-supported-macos = setting the date is not supported by macOS date-error-setting-date-not-supported-redox = setting the date is not supported by Redox date-error-cannot-set-date = cannot set date +date-error-extra-operand = extra operand '{$operand}' diff --git a/src/uu/date/locales/fr-FR.ftl b/src/uu/date/locales/fr-FR.ftl index 204121f9218..2529b42635a 100644 --- a/src/uu/date/locales/fr-FR.ftl +++ b/src/uu/date/locales/fr-FR.ftl @@ -99,3 +99,4 @@ date-error-date-overflow = débordement de date '{$date}' date-error-setting-date-not-supported-macos = la définition de la date n'est pas prise en charge par macOS date-error-setting-date-not-supported-redox = la définition de la date n'est pas prise en charge par Redox date-error-cannot-set-date = impossible de définir la date +date-error-extra-operand = opérande supplémentaire '{$operand}' diff --git a/src/uu/date/src/date.rs b/src/uu/date/src/date.rs index 4814c4a59a0..3e6b67c6602 100644 --- a/src/uu/date/src/date.rs +++ b/src/uu/date/src/date.rs @@ -171,7 +171,28 @@ fn parse_military_timezone_with_offset(s: &str) -> Option { #[uucore::main] #[allow(clippy::cognitive_complexity)] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?; + let args: Vec = args.collect(); + let matches = match uu_app().try_get_matches_from(&args) { + Ok(matches) => matches, + Err(e) => { + match e.kind() { + clap::error::ErrorKind::DisplayHelp | clap::error::ErrorKind::DisplayVersion => { + return Err(e.into()); + } + _ => { + // Convert unknown options to be treated as invalid date format + // This ensures consistent exit status 1 instead of clap's exit status 77 + if let Some(arg) = args.get(1) { + return Err(USimpleError::new( + 1, + translate!("date-error-invalid-date", "date" => arg.to_string_lossy()), + )); + } + return Err(USimpleError::new(1, e.to_string())); + } + } + } + }; // Check for extra operands (multiple positional arguments) if let Some(formats) = matches.get_many::(OPT_FORMAT) { @@ -524,7 +545,7 @@ pub fn uu_app() -> Command { .help(translate!("date-help-universal")) .action(ArgAction::SetTrue), ) - .arg(Arg::new(OPT_FORMAT).num_args(0..)) + .arg(Arg::new(OPT_FORMAT).num_args(0..).trailing_var_arg(true)) } /// Return the appropriate format string for the given settings. diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index 0664af3b0e6..cbe48d5b4f7 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -32,6 +32,30 @@ fn test_extra_operands() { .stderr_contains("extra operand 'extra'"); } +#[test] +fn test_invalid_long_option() { + new_ucmd!() + .arg("--fB") + .fails_with_code(1) + .stderr_contains("invalid date '--fB'"); +} + +#[test] +fn test_invalid_short_option() { + new_ucmd!() + .arg("-w") + .fails_with_code(1) + .stderr_contains("invalid date '-w'"); +} + +#[test] +fn test_single_dash_as_date() { + new_ucmd!() + .arg("-") + .fails_with_code(1) + .stderr_contains("invalid date"); +} + #[test] fn test_date_email() { for param in ["--rfc-email", "--rfc-e", "-R", "--rfc-2822", "--rfc-822"] { From f5f107978392f14af072a7e06d9147ae34c40d3c Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 2 Nov 2025 23:16:55 +0100 Subject: [PATCH 4/5] date: improve the date fuzzer --- fuzz/fuzz_targets/fuzz_date.rs | 36 +++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/fuzz/fuzz_targets/fuzz_date.rs b/fuzz/fuzz_targets/fuzz_date.rs index a52788a6cbe..16a792105ce 100644 --- a/fuzz/fuzz_targets/fuzz_date.rs +++ b/fuzz/fuzz_targets/fuzz_date.rs @@ -7,20 +7,34 @@ use uufuzz::generate_and_run_uumain; fuzz_target!(|data: &[u8]| { let delim: u8 = 0; // Null byte - let args: Vec = data + let fuzz_args: Vec = data .split(|b| *b == delim) .filter_map(|e| std::str::from_utf8(e).ok()) .map(OsString::from) .collect(); - - // Ensure we have at least a program name - if args.is_empty() { - return; + + // Skip test cases that would cause the program to read from stdin + // These would hang the fuzzer waiting for input + for i in 0..fuzz_args.len() { + if let Some(arg) = fuzz_args.get(i) { + let arg_str = arg.to_string_lossy(); + // Skip if -f- or --file=- (reads dates from stdin) + if (arg_str == "-f" + && fuzz_args + .get(i + 1) + .map(|a| a.to_string_lossy() == "-") + .unwrap_or(false)) + || arg_str == "-f-" + || arg_str == "--file=-" + { + return; + } + } } - - let date_main = |args: std::vec::IntoIter| -> i32 { - uumain(args) - }; - - let _ = generate_and_run_uumain(&args, date_main, None); + + // Add program name as first argument (required for proper argument parsing) + let mut args = vec![OsString::from("date")]; + args.extend(fuzz_args); + + let _ = generate_and_run_uumain(&args, uumain, None); }); From 5e90bbf325df34a5090a76ee1f09a12701cba0a5 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 2 Nov 2025 23:17:06 +0100 Subject: [PATCH 5/5] date fuzzer: should pass in the CI --- .github/workflows/fuzzing.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/fuzzing.yml b/.github/workflows/fuzzing.yml index 42c24710612..b5bfd955428 100644 --- a/.github/workflows/fuzzing.yml +++ b/.github/workflows/fuzzing.yml @@ -79,8 +79,7 @@ jobs: matrix: test-target: - { name: fuzz_test, should_pass: true } - # https://github.com/uutils/coreutils/issues/5311 - - { name: fuzz_date, should_pass: false } + - { name: fuzz_date, should_pass: true } - { name: fuzz_expr, should_pass: true } - { name: fuzz_printf, should_pass: true } - { name: fuzz_echo, should_pass: true }