diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index 8848b6af1..d9a4ade14 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -300,22 +300,22 @@ jobs: run: make nextest PROFILE=ci CARGOFLAGS="--hide-progress-bar" env: RUST_BACKTRACE: "1" - - - name: "`make install PROFILE=release-fast COMPLETIONS=n MANPAGES=n LOCALES=n`" + - name: "`make install PROG_PREFIX=uu- PROFILE=release-fast COMPLETIONS=n MANPAGES=n LOCALES=n`" shell: bash run: | set -x - DESTDIR=/tmp/ make PROFILE=release-fast COMPLETIONS=n MANPAGES=n LOCALES=n install + DESTDIR=/tmp/ make install PROG_PREFIX=uu- PROFILE=release-fast COMPLETIONS=n MANPAGES=n LOCALES=n # Check that utils are built with given profile ./target/release-fast/true - # Check that the utils are present - test -f /tmp/usr/local/bin/tty + # Check that the progs have prefix + test -f /tmp/usr/local/bin/uu-tty + test -f /tmp/usr/local/libexec/uu-coreutils/libstdbuf.* # Check that the manpage is not present - ! test -f /tmp/usr/local/share/man/man1/whoami.1 + ! test -f /tmp/usr/local/share/man/man1/uu-whoami.1 # Check that the completion is not present - ! test -f /tmp/usr/local/share/zsh/site-functions/_install - ! test -f /tmp/usr/local/share/bash-completion/completions/head.bash - ! test -f /tmp/usr/local/share/fish/vendor_completions.d/cat.fish + ! test -f /tmp/usr/local/share/zsh/site-functions/_uu-install + ! test -f /tmp/usr/local/share/bash-completion/completions/uu-head.bash + ! test -f /tmp/usr/local/share/fish/vendor_completions.d/uu-cat.fish env: RUST_BACKTRACE: "1" - name: "`make install`" diff --git a/.github/workflows/fuzzing.yml b/.github/workflows/fuzzing.yml index a8cb5fd65..aaf7080e6 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 } diff --git a/Cargo.lock b/Cargo.lock index 37b3362e6..4a28fb1dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3126,7 +3126,6 @@ dependencies = [ "clap", "codspeed-divan-compat", "fluent", - "hex", "tempfile", "uucore", ] diff --git a/GNUmakefile b/GNUmakefile index ceb48d2d1..6f5eda35f 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -27,20 +27,20 @@ CARGO ?= cargo CARGOFLAGS ?= RUSTC_ARCH ?= # should be empty except for cross-build, not --target $(shell rustc --print host-tuple) +#prefix prepended to all binaries and library dir +PROG_PREFIX ?= + # Install directories PREFIX ?= /usr/local DESTDIR ?= BINDIR ?= $(PREFIX)/bin DATAROOTDIR ?= $(PREFIX)/share -LIBSTDBUF_DIR ?= $(PREFIX)/libexec/coreutils +LIBSTDBUF_DIR ?= $(PREFIX)/libexec/$(PROG_PREFIX)coreutils # Export variable so that it is used during the build export LIBSTDBUF_DIR INSTALLDIR_BIN=$(DESTDIR)$(BINDIR) -#prefix to apply to coreutils binary and all tool binaries -PROG_PREFIX ?= - # This won't support any directory with spaces in its name, but you can just # make a symlink without spaces that points to the directory. BASEDIR ?= $(shell pwd) diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index 90934a271..2b519a989 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -1575,7 +1575,6 @@ version = "0.5.0" dependencies = [ "clap", "fluent", - "hex", "uucore", ] diff --git a/fuzz/fuzz_targets/fuzz_date.rs b/fuzz/fuzz_targets/fuzz_date.rs index 0f9cb262c..16a792105 100644 --- a/fuzz/fuzz_targets/fuzz_date.rs +++ b/fuzz/fuzz_targets/fuzz_date.rs @@ -3,12 +3,38 @@ 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 fuzz_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(); + + // 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; + } + } + } + + // 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); }); diff --git a/src/uu/cksum/Cargo.toml b/src/uu/cksum/Cargo.toml index 7e62c5c8f..840397273 100644 --- a/src/uu/cksum/Cargo.toml +++ b/src/uu/cksum/Cargo.toml @@ -25,7 +25,6 @@ uucore = { workspace = true, features = [ "sum", "hardware", ] } -hex = { workspace = true } fluent = { workspace = true } [dev-dependencies] diff --git a/src/uu/cksum/src/cksum.rs b/src/uu/cksum/src/cksum.rs index 666a0e982..30eabcaac 100644 --- a/src/uu/cksum/src/cksum.rs +++ b/src/uu/cksum/src/cksum.rs @@ -22,7 +22,7 @@ use uucore::checksum::{ use uucore::error::UResult; use uucore::hardware::{HasHardwareFeatures as _, SimdPolicy}; use uucore::line_ending::LineEnding; -use uucore::{format_usage, translate}; +use uucore::{format_usage, show_error, translate}; /// Print CPU hardware capability detection information to stderr /// This matches GNU cksum's --debug behavior @@ -31,9 +31,9 @@ fn print_cpu_debug_info() { fn print_feature(name: &str, available: bool) { if available { - eprintln!("cksum: using {name} hardware support"); + show_error!("using {name} hardware support"); } else { - eprintln!("cksum: {name} support not detected"); + show_error!("{name} support not detected"); } } @@ -140,6 +140,20 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let check = matches.get_flag(options::CHECK); + let check_flag = |flag| match (check, matches.get_flag(flag)) { + (_, false) => Ok(false), + (true, true) => Ok(true), + (false, true) => Err(ChecksumError::CheckOnlyFlag(flag.into())), + }; + + // Each of the following flags are only expected in --check mode. + // If we encounter them otherwise, end with an error. + let ignore_missing = check_flag(options::IGNORE_MISSING)?; + let warn = check_flag(options::WARN)?; + let quiet = check_flag(options::QUIET)?; + let strict = check_flag(options::STRICT)?; + let status = check_flag(options::STATUS)?; + let algo_cli = matches .get_one::(options::ALGORITHM) .map(AlgoKind::from_cksum) @@ -166,11 +180,6 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let text_flag = matches.get_flag(options::TEXT); let binary_flag = matches.get_flag(options::BINARY); - let strict = matches.get_flag(options::STRICT); - let status = matches.get_flag(options::STATUS); - let warn = matches.get_flag(options::WARN); - let ignore_missing = matches.get_flag(options::IGNORE_MISSING); - let quiet = matches.get_flag(options::QUIET); let tag = matches.get_flag(options::TAG); if tag || binary_flag || text_flag { @@ -191,6 +200,11 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // Not --check + // Print hardware debug info if requested + if matches.get_flag(options::DEBUG) { + print_cpu_debug_info(); + } + // Set the default algorithm to CRC when not '--check'ing. let algo_kind = algo_cli.unwrap_or(AlgoKind::Crc); @@ -199,22 +213,15 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let algo = SizedAlgoKind::from_unsized(algo_kind, length)?; let line_ending = LineEnding::from_zero_flag(matches.get_flag(options::ZERO)); - let output_format = figure_out_output_format( - algo, - tag, - binary, - matches.get_flag(options::RAW), - matches.get_flag(options::BASE64), - ); - - // Print hardware debug info if requested - if matches.get_flag(options::DEBUG) { - print_cpu_debug_info(); - } - let opts = ChecksumComputeOptions { algo_kind: algo, - output_format, + output_format: figure_out_output_format( + algo, + tag, + binary, + matches.get_flag(options::RAW), + matches.get_flag(options::BASE64), + ), line_ending, }; diff --git a/src/uu/date/locales/en-US.ftl b/src/uu/date/locales/en-US.ftl index 72113c405..b320cefef 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 204121f92..2529b4263 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 93c085466..d02ca4a47 100644 --- a/src/uu/date/src/date.rs +++ b/src/uu/date/src/date.rs @@ -171,6 +171,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( @@ -515,7 +526,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..).trailing_var_arg(true)) } /// Return the appropriate format string for the given settings. diff --git a/src/uu/hashsum/src/hashsum.rs b/src/uu/hashsum/src/hashsum.rs index a096238f9..047d6889c 100644 --- a/src/uu/hashsum/src/hashsum.rs +++ b/src/uu/hashsum/src/hashsum.rs @@ -164,16 +164,27 @@ pub fn uumain(mut args: impl uucore::Args) -> UResult<()> { binary_flag_default }; let check = matches.get_flag("check"); - let status = matches.get_flag("status"); - let quiet = matches.get_flag("quiet"); - let strict = matches.get_flag("strict"); - let warn = matches.get_flag("warn"); - let ignore_missing = matches.get_flag("ignore-missing"); - if ignore_missing && !check { - // --ignore-missing needs -c - return Err(ChecksumError::IgnoreNotCheck.into()); - } + let check_flag = |flag| match (check, matches.get_flag(flag)) { + (_, false) => Ok(false), + (true, true) => Ok(true), + (false, true) => Err(ChecksumError::CheckOnlyFlag(flag.into())), + }; + + // Each of the following flags are only expected in --check mode. + // If we encounter them otherwise, end with an error. + let ignore_missing = check_flag("ignore-missing")?; + let warn = check_flag("warn")?; + let quiet = check_flag("quiet")?; + let strict = check_flag("strict")?; + let status = check_flag("status")?; + + let files = matches.get_many::(options::FILE).map_or_else( + // No files given, read from stdin. + || Box::new(iter::once(OsStr::new("-"))) as Box>, + // At least one file given, read from them. + |files| Box::new(files.map(OsStr::new)) as Box>, + ); if check { // on Windows, allow --binary/--text to be used with --check @@ -188,13 +199,6 @@ pub fn uumain(mut args: impl uucore::Args) -> UResult<()> { } } - // Execute the checksum validation based on the presence of files or the use of stdin - // Determine the source of input: a list of files or stdin. - let input = matches.get_many::(options::FILE).map_or_else( - || iter::once(OsStr::new("-")).collect::>(), - |files| files.map(OsStr::new).collect::>(), - ); - let verbose = ChecksumVerbose::new(status, quiet, warn); let opts = ChecksumValidateOptions { @@ -204,16 +208,11 @@ pub fn uumain(mut args: impl uucore::Args) -> UResult<()> { }; // Execute the checksum validation - return perform_checksum_validation(input.iter().copied(), Some(algo_kind), length, opts); - } else if quiet { - return Err(ChecksumError::QuietNotCheck.into()); - } else if strict { - return Err(ChecksumError::StrictNotCheck.into()); + return perform_checksum_validation(files, Some(algo_kind), length, opts); } - let line_ending = LineEnding::from_zero_flag(matches.get_flag("zero")); - let algo = SizedAlgoKind::from_unsized(algo_kind, length)?; + let line_ending = LineEnding::from_zero_flag(matches.get_flag("zero")); let opts = ChecksumComputeOptions { algo_kind: algo, @@ -227,13 +226,6 @@ pub fn uumain(mut args: impl uucore::Args) -> UResult<()> { line_ending, }; - let files = matches.get_many::(options::FILE).map_or_else( - // No files given, read from stdin. - || Box::new(iter::once(OsStr::new("-"))) as Box>, - // At least one file given, read from them. - |files| Box::new(files.map(OsStr::new)) as Box>, - ); - // Show the hashsum of the input perform_checksum_computation(opts, files) } diff --git a/src/uu/printenv/src/printenv.rs b/src/uu/printenv/src/printenv.rs index 47801fd37..fb0224748 100644 --- a/src/uu/printenv/src/printenv.rs +++ b/src/uu/printenv/src/printenv.rs @@ -3,10 +3,14 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use clap::{Arg, ArgAction, Command}; use std::env; -use uucore::translate; -use uucore::{error::UResult, format_usage}; +use std::io::Write; + +use clap::{Arg, ArgAction, Command}; + +use uucore::error::UResult; +use uucore::line_ending::LineEnding; +use uucore::{format_usage, os_str_as_bytes, translate}; static OPT_NULL: &str = "null"; @@ -21,15 +25,16 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { .map(|v| v.map(ToString::to_string).collect()) .unwrap_or_default(); - let separator = if matches.get_flag(OPT_NULL) { - "\x00" - } else { - "\n" - }; + let separator = LineEnding::from_zero_flag(matches.get_flag(OPT_NULL)); if variables.is_empty() { - for (env_var, value) in env::vars() { - print!("{env_var}={value}{separator}"); + for (env_var, value) in env::vars_os() { + let env_bytes = os_str_as_bytes(&env_var)?; + let val_bytes = os_str_as_bytes(&value)?; + std::io::stdout().lock().write_all(env_bytes)?; + print!("="); + std::io::stdout().lock().write_all(val_bytes)?; + print!("{separator}"); } return Ok(()); } @@ -41,8 +46,10 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { error_found = true; continue; } - if let Ok(var) = env::var(env_var) { - print!("{var}{separator}"); + if let Some(var) = env::var_os(env_var) { + let val_bytes = os_str_as_bytes(&var)?; + std::io::stdout().lock().write_all(val_bytes)?; + print!("{separator}"); } else { error_found = true; } diff --git a/src/uucore/src/lib/features/checksum/mod.rs b/src/uucore/src/lib/features/checksum/mod.rs index 455a4e1bf..2f3d28b41 100644 --- a/src/uucore/src/lib/features/checksum/mod.rs +++ b/src/uucore/src/lib/features/checksum/mod.rs @@ -373,12 +373,9 @@ impl SizedAlgoKind { pub enum ChecksumError { #[error("the --raw option is not supported with multiple files")] RawMultipleFiles, - #[error("the --ignore-missing option is meaningful only when verifying checksums")] - IgnoreNotCheck, - #[error("the --strict option is meaningful only when verifying checksums")] - StrictNotCheck, - #[error("the --quiet option is meaningful only when verifying checksums")] - QuietNotCheck, + + #[error("the --{0} option is meaningful only when verifying checksums")] + CheckOnlyFlag(String), // --length sanitization errors #[error("--length required for {}", .0.quote())] diff --git a/src/uucore/src/lib/mods/clap_localization.rs b/src/uucore/src/lib/mods/clap_localization.rs index 5a54bf7c3..e0a0ce84e 100644 --- a/src/uucore/src/lib/mods/clap_localization.rs +++ b/src/uucore/src/lib/mods/clap_localization.rs @@ -11,7 +11,7 @@ //! instead of parsing error strings, providing a more robust solution. //! -use crate::error::UResult; +use crate::error::{UResult, USimpleError}; use crate::locale::translate; use clap::error::{ContextKind, ErrorKind}; @@ -108,43 +108,37 @@ impl<'a> ErrorFormatter<'a> { where F: FnOnce(), { + let code = self.print_error(err, exit_code); + callback(); + std::process::exit(code); + } + + /// Print error and return exit code (no exit call) + pub fn print_error(&self, err: &Error, exit_code: i32) -> i32 { match err.kind() { ErrorKind::DisplayHelp | ErrorKind::DisplayVersion => self.handle_display_errors(err), - ErrorKind::UnknownArgument => { - self.handle_unknown_argument_with_callback(err, exit_code, callback) - } + ErrorKind::UnknownArgument => self.handle_unknown_argument(err, exit_code), ErrorKind::InvalidValue | ErrorKind::ValueValidation => { - self.handle_invalid_value_with_callback(err, exit_code, callback) - } - ErrorKind::MissingRequiredArgument => { - self.handle_missing_required_with_callback(err, exit_code, callback) + self.handle_invalid_value(err, exit_code) } + ErrorKind::MissingRequiredArgument => self.handle_missing_required(err, exit_code), ErrorKind::TooFewValues | ErrorKind::TooManyValues | ErrorKind::WrongNumberOfValues => { // These need full clap formatting eprint!("{}", err.render()); - callback(); - std::process::exit(exit_code); + exit_code } - _ => self.handle_generic_error_with_callback(err, exit_code, callback), + _ => self.handle_generic_error(err, exit_code), } } /// Handle help and version display - fn handle_display_errors(&self, err: &Error) -> ! { + fn handle_display_errors(&self, err: &Error) -> i32 { print!("{}", err.render()); - std::process::exit(0); + 0 } - /// Handle unknown argument errors with callback - fn handle_unknown_argument_with_callback( - &self, - err: &Error, - exit_code: i32, - callback: F, - ) -> ! - where - F: FnOnce(), - { + /// Handle unknown argument errors + fn handle_unknown_argument(&self, err: &Error, exit_code: i32) -> i32 { if let Some(invalid_arg) = err.get(ContextKind::InvalidArg) { let arg_str = invalid_arg.to_string(); let error_word = translate!("common-error"); @@ -179,21 +173,13 @@ impl<'a> ErrorFormatter<'a> { self.print_usage_and_help(); } else { - self.print_simple_error_with_callback( - &translate!("clap-error-unexpected-argument-simple"), - exit_code, - || {}, - ); + self.print_simple_error_msg(&translate!("clap-error-unexpected-argument-simple")); } - callback(); - std::process::exit(exit_code); + exit_code } - /// Handle invalid value errors with callback - fn handle_invalid_value_with_callback(&self, err: &Error, exit_code: i32, callback: F) -> ! - where - F: FnOnce(), - { + /// Handle invalid value errors + fn handle_invalid_value(&self, err: &Error, exit_code: i32) -> i32 { let invalid_arg = err.get(ContextKind::InvalidArg); let invalid_value = err.get(ContextKind::InvalidValue); @@ -245,32 +231,22 @@ impl<'a> ErrorFormatter<'a> { eprintln!(); eprintln!("{}", translate!("common-help-suggestion")); } else { - self.print_simple_error(&err.render().to_string(), exit_code); + self.print_simple_error_msg(&err.render().to_string()); } // InvalidValue errors traditionally use exit code 1 for backward compatibility // But if a utility explicitly requests a high exit code (>= 125), respect it // This allows utilities like runcon (125) to override the default while preserving // the standard behavior for utilities using normal error codes (1, 2, etc.) - let actual_exit_code = if matches!(err.kind(), ErrorKind::InvalidValue) && exit_code < 125 { + if matches!(err.kind(), ErrorKind::InvalidValue) && exit_code < 125 { 1 // Force exit code 1 for InvalidValue unless using special exit codes } else { exit_code // Respect the requested exit code for special cases - }; - callback(); - std::process::exit(actual_exit_code); + } } - /// Handle missing required argument errors with callback - fn handle_missing_required_with_callback( - &self, - err: &Error, - exit_code: i32, - callback: F, - ) -> ! - where - F: FnOnce(), - { + /// Handle missing required argument errors + fn handle_missing_required(&self, err: &Error, exit_code: i32) -> i32 { let rendered_str = err.render().to_string(); let lines: Vec<&str> = rendered_str.lines().collect(); @@ -313,15 +289,11 @@ impl<'a> ErrorFormatter<'a> { } _ => eprint!("{}", err.render()), } - callback(); - std::process::exit(exit_code); + exit_code } - /// Handle generic errors with callback - fn handle_generic_error_with_callback(&self, err: &Error, exit_code: i32, callback: F) -> ! - where - F: FnOnce(), - { + /// Handle generic errors + fn handle_generic_error(&self, err: &Error, exit_code: i32) -> i32 { let rendered_str = err.render().to_string(); if let Some(main_error_line) = rendered_str.lines().next() { self.print_localized_error_line(main_error_line); @@ -330,27 +302,16 @@ impl<'a> ErrorFormatter<'a> { } else { eprint!("{}", err.render()); } - callback(); - std::process::exit(exit_code); + exit_code } - /// Print a simple error message - fn print_simple_error(&self, message: &str, exit_code: i32) -> ! { - self.print_simple_error_with_callback(message, exit_code, || {}) - } - - /// Print a simple error message with callback - fn print_simple_error_with_callback(&self, message: &str, exit_code: i32, callback: F) -> ! - where - F: FnOnce(), - { + /// Print a simple error message (no exit) + fn print_simple_error_msg(&self, message: &str) { let error_word = translate!("common-error"); eprintln!( "{}: {message}", self.color_mgr.colorize(&error_word, Color::Red) ); - callback(); - std::process::exit(exit_code); } /// Print error line with localized "error:" prefix @@ -478,7 +439,9 @@ where if e.exit_code() == 0 { e.into() // Preserve help/version } else { - handle_clap_error_with_exit_code(e, exit_code) + let formatter = ErrorFormatter::new(crate::util_name()); + let code = formatter.print_error(&e, exit_code); + USimpleError::new(code, "") } }) } diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index bd1c31cc1..319e3ab03 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -17,6 +17,45 @@ 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_extra_operands() { + new_ucmd!() + .args(&["test", "extra"]) + .fails_with_code(1) + .stderr_contains("extra operand 'extra'"); +} + +#[test] +fn test_invalid_long_option() { + new_ucmd!() + .arg("--fB") + .fails_with_code(1) + .stderr_contains("unexpected argument '--fB'"); +} + +#[test] +fn test_invalid_short_option() { + new_ucmd!() + .arg("-w") + .fails_with_code(1) + .stderr_contains("unexpected argument '-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"] { diff --git a/tests/by-util/test_printenv.rs b/tests/by-util/test_printenv.rs index 4c1b436bc..71f22c984 100644 --- a/tests/by-util/test_printenv.rs +++ b/tests/by-util/test_printenv.rs @@ -90,3 +90,30 @@ fn test_null_separator() { .stdout_is("FOO\x00VALUE\x00"); } } + +#[test] +#[cfg(unix)] +#[cfg(not(any(target_os = "freebsd", target_os = "android", target_os = "openbsd")))] +fn test_non_utf8_value() { + use std::ffi::OsStr; + use std::os::unix::ffi::OsStrExt; + // Environment variable values can contain non-UTF-8 bytes on Unix. + // printenv should output them correctly, matching GNU behavior. + // Reproduces: LD_PRELOAD=$'/tmp/lib.so\xff' printenv LD_PRELOAD + let value_with_invalid_utf8 = OsStr::from_bytes(b"/tmp/lib.so\xff"); + + let result = new_ucmd!() + .env("LD_PRELOAD", value_with_invalid_utf8) + .arg("LD_PRELOAD") + .run(); + + // Use byte-based assertions to avoid UTF-8 conversion issues + // when the test framework tries to format error messages + assert!( + result.succeeded(), + "Command failed with exit code: {:?}, stderr: {:?}", + result.code(), + String::from_utf8_lossy(result.stderr()) + ); + result.stdout_is_bytes(b"/tmp/lib.so\xff\n"); +}