diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index c5bbf7f11..7c62cc0cb 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -379,6 +379,11 @@ jobs: set -x # Regression-test for https://github.com/uutils/coreutils/issues/8701 make UTILS="rm chmod chown chgrp mv du" + # Verifies that + # 1. there is no "error: none of the selected packages contains this + # feature: feat_external_libstdbuf" + # 2. the makefile doesn't try to install libstdbuf even though stdbuf is skipped + DESTDIR=/tmp/ make SKIP_UTILS="stdbuf" install build_rust_stable: name: Build/stable diff --git a/GNUmakefile b/GNUmakefile index 41ba59349..11c83f9fd 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -107,6 +107,7 @@ PROGS := \ dir \ dircolors \ dirname \ + du \ echo \ env \ expand \ @@ -117,6 +118,7 @@ PROGS := \ fold \ hashsum \ head \ + hostname \ join \ link \ ln \ @@ -145,17 +147,18 @@ PROGS := \ sleep \ sort \ split \ - stty \ sum \ sync \ tac \ tail \ tee \ test \ + touch \ tr \ true \ truncate \ tsort \ + uname \ unexpand \ uniq \ vdir \ @@ -169,10 +172,8 @@ UNIX_PROGS := \ chmod \ chown \ chroot \ - du \ groups \ hostid \ - hostname \ id \ install \ kill \ @@ -185,10 +186,9 @@ UNIX_PROGS := \ pinky \ stat \ stdbuf \ + stty \ timeout \ - touch \ tty \ - uname \ unlink \ uptime \ users \ @@ -228,7 +228,7 @@ ifneq ($(OS),Windows_NT) PROGS := $(PROGS) $(SELINUX_PROGS) endif -UTILS ?= $(PROGS) +UTILS ?= $(filter-out $(SKIP_UTILS),$(PROGS)) ifneq ($(findstring stdbuf,$(UTILS)),) # Use external libstdbuf per default. It is more robust than embedding libstdbuf. @@ -306,7 +306,7 @@ TEST_PROGS := \ who TESTS := \ - $(sort $(filter $(UTILS),$(filter-out $(SKIP_UTILS),$(TEST_PROGS)))) + $(sort $(filter $(UTILS),$(TEST_PROGS))) TEST_NO_FAIL_FAST := TEST_SPEC_FEATURE := @@ -326,7 +326,7 @@ endef # Output names EXES := \ - $(sort $(filter $(UTILS),$(filter-out $(SKIP_UTILS),$(PROGS)))) + $(sort $(UTILS)) INSTALLEES := ${EXES} ifeq (${MULTICALL}, y) @@ -352,7 +352,7 @@ build-coreutils: build: build-coreutils build-pkgs locales -$(foreach test,$(filter-out $(SKIP_UTILS),$(PROGS)),$(eval $(call TEST_BUSYBOX,$(test)))) +$(foreach test,$(UTILS),$(eval $(call TEST_BUSYBOX,$(test)))) test: ${CARGO} test ${CARGOFLAGS} --features "$(TESTS) $(TEST_SPEC_FEATURE)" --no-default-features $(TEST_NO_FAIL_FAST) @@ -482,7 +482,7 @@ endif install: build install-manpages install-completions install-locales mkdir -p $(INSTALLDIR_BIN) -ifneq ($(OS),Windows_NT) +ifneq (,$(and $(findstring stdbuf,$(UTILS)),$(findstring feat_external_libstdbuf,$(CARGOFLAGS)))) mkdir -p $(DESTDIR)$(LIBSTDBUF_DIR) $(INSTALL) -m 755 $(BUILDDIR)/deps/libstdbuf* $(DESTDIR)$(LIBSTDBUF_DIR)/ endif diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 142821224..d3c987f22 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -18,6 +18,11 @@ license = "MIT" [package.metadata] cargo-fuzz = true +# Enable debug symbols in release builds for readable backtraces +# when fuzzing discovers crashes. This addresses issue #5343. +[profile.release] +debug = true + [dependencies] libfuzzer-sys = "0.4.7" rand = { version = "0.9.0", features = ["small_rng"] } diff --git a/src/uu/df/src/blocks.rs b/src/uu/df/src/blocks.rs index d6842f369..ded8b5912 100644 --- a/src/uu/df/src/blocks.rs +++ b/src/uu/df/src/blocks.rs @@ -9,7 +9,7 @@ use std::{env, fmt}; use uucore::{ display::Quotable, - parser::parse_size::{ParseSizeError, parse_size_u64}, + parser::parse_size::{ParseSizeError, parse_size_non_zero_u64, parse_size_u64}, }; /// The first ten powers of 1024. @@ -213,7 +213,7 @@ pub(crate) fn read_block_size(matches: &ArgMatches) -> Result Option { for env_var in ["DF_BLOCK_SIZE", "BLOCK_SIZE", "BLOCKSIZE"] { if let Ok(env_size) = env::var(env_var) { - return parse_size_u64(&env_size).ok(); + return parse_size_non_zero_u64(&env_size).ok(); } } diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 5d3136a58..064e1b1e5 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -68,7 +68,7 @@ use uucore::{ line_ending::LineEnding, os_str_as_bytes_lossy, parser::parse_glob, - parser::parse_size::parse_size_u64, + parser::parse_size::parse_size_non_zero_u64, parser::shortcut_value_parser::ShortcutValueParser, quoting_style::{QuotingStyle, locale_aware_escape_dir_name, locale_aware_escape_name}, show, show_error, show_warning, @@ -902,7 +902,7 @@ impl Config { let (file_size_block_size, block_size) = if !opt_si && !opt_hr && !raw_block_size.is_empty() { - match parse_size_u64(&raw_block_size.to_string_lossy()) { + match parse_size_non_zero_u64(&raw_block_size.to_string_lossy()) { Ok(size) => match (is_env_var_blocksize, opt_kb) { (true, true) => (DEFAULT_FILE_SIZE_BLOCK_SIZE, DEFAULT_BLOCK_SIZE), (true, false) => (DEFAULT_FILE_SIZE_BLOCK_SIZE, size), diff --git a/src/uu/od/locales/en-US.ftl b/src/uu/od/locales/en-US.ftl index 38bf1c577..208bd3333 100644 --- a/src/uu/od/locales/en-US.ftl +++ b/src/uu/od/locales/en-US.ftl @@ -66,6 +66,7 @@ od-help-address-radix = Select the base in which file offsets are printed. od-help-skip-bytes = Skip bytes input bytes before formatting and writing. od-help-read-bytes = limit dump to BYTES input bytes od-help-endian = byte order to use for multi-byte formats +od-help-strings = output strings of at least BYTES graphic chars. 3 is assumed when BYTES is not specified. od-help-a = named characters, ignoring high-order bit od-help-b = octal bytes od-help-c = ASCII characters or backslash escapes @@ -76,3 +77,17 @@ od-help-output-duplicates = do not use * to mark line suppression od-help-width = output BYTES bytes per output line. 32 is implied when BYTES is not specified. od-help-traditional = compatibility mode with one input, offset and label. +od-help-o = octal 2-byte units +od-help-capital-i = decimal 8-byte units +od-help-capital-l = decimal 8-byte units +od-help-i = decimal 4-byte units +od-help-l = decimal 8-byte units +od-help-x = hexadecimal 2-byte units +od-help-h = hexadecimal 2-byte units +od-help-capital-o = octal 4-byte units +od-help-s = decimal 2-byte units +od-help-capital-x = hexadecimal 4-byte units +od-help-capital-h = hexadecimal 4-byte units +od-help-e = floating point double precision (64-bit) units +od-help-f = floating point double precision (32-bit) units +od-help-capital-f = floating point double precision (64-bit) units diff --git a/src/uu/od/locales/fr-FR.ftl b/src/uu/od/locales/fr-FR.ftl index 712550667..cba433b64 100644 --- a/src/uu/od/locales/fr-FR.ftl +++ b/src/uu/od/locales/fr-FR.ftl @@ -67,6 +67,7 @@ od-help-address-radix = Sélectionner la base dans laquelle les décalages de fi od-help-skip-bytes = Ignorer les octets d'entrée avant le formatage et l'écriture. od-help-read-bytes = limiter le dump à OCTETS octets d'entrée od-help-endian = ordre des octets à utiliser pour les formats multi-octets +od-help-strings = afficher les chaînes d'au moins OCTETS caractères graphiques. 3 est supposé quand OCTETS n'est pas spécifié. od-help-a = caractères nommés, ignorant le bit d'ordre supérieur od-help-b = octets octaux od-help-c = caractères ASCII ou échappements antislash @@ -77,3 +78,17 @@ od-help-output-duplicates = ne pas utiliser * pour marquer la suppression de lig od-help-width = sortir OCTETS octets par ligne de sortie. 32 est impliqué quand OCTETS n'est pas spécifié. od-help-traditional = mode de compatibilité avec une entrée, décalage et étiquette. +od-help-o = unités octales 2-octets +od-help-capital-i = unités décimales 8-octets +od-help-capital-l = unités décimales 8-octets +od-help-i = unités décimales 4-octets +od-help-l = unités décimales 8-octets +od-help-x = unités hexadécimales 2-octets +od-help-h = unités hexadécimales 2-octets +od-help-capital-o = unités octales 4-octets +od-help-s = unités décimales 2-octets +od-help-capital-x = unités hexadécimales 4-octets +od-help-capital-h = unités hexadécimales 4-octets +od-help-e = unités virgule flottante double précision (64-bits) +od-help-f = unités virgule flottante double précision (32-bits) +od-help-capital-f = unités virgule flottante double précision (64-bits) diff --git a/src/uu/od/src/od.rs b/src/uu/od/src/od.rs index e4a8ce108..80e6893d1 100644 --- a/src/uu/od/src/od.rs +++ b/src/uu/od/src/od.rs @@ -26,7 +26,7 @@ mod prn_int; use std::cmp; use std::fmt::Write; -use std::io::BufReader; +use std::io::{BufReader, Read}; use crate::byteorder_io::ByteOrder; use crate::formatter_item_info::FormatWriter; @@ -76,6 +76,21 @@ struct OdOptions { line_bytes: usize, output_duplicates: bool, radix: Radix, + string_min_length: Option, +} + +/// Helper function to parse bytes with error handling +fn parse_bytes_option(matches: &ArgMatches, option_name: &str) -> UResult> { + match matches.get_one::(option_name) { + None => Ok(None), + Some(s) => match parse_number_of_bytes(s) { + Ok(n) => Ok(Some(n)), + Err(e) => Err(USimpleError::new( + 1, + format_error_message(&e, s, option_name), + )), + }, + } } impl OdOptions { @@ -95,18 +110,7 @@ impl OdOptions { ByteOrder::Native }; - let mut skip_bytes = match matches.get_one::(options::SKIP_BYTES) { - None => 0, - Some(s) => match parse_number_of_bytes(s) { - Ok(n) => n, - Err(e) => { - return Err(USimpleError::new( - 1, - format_error_message(&e, s, options::SKIP_BYTES), - )); - } - }, - }; + let mut skip_bytes = parse_bytes_option(matches, options::SKIP_BYTES)?.unwrap_or(0); let mut label: Option = None; @@ -156,17 +160,16 @@ impl OdOptions { let output_duplicates = matches.get_flag(options::OUTPUT_DUPLICATES); - let read_bytes = match matches.get_one::(options::READ_BYTES) { + let read_bytes = parse_bytes_option(matches, options::READ_BYTES)?; + + let string_min_length = match parse_bytes_option(matches, options::STRINGS)? { None => None, - Some(s) => match parse_number_of_bytes(s) { - Ok(n) => Some(n), - Err(e) => { - return Err(USimpleError::new( - 1, - format_error_message(&e, s, options::READ_BYTES), - )); - } - }, + Some(n) => Some(usize::try_from(n).map_err(|_| { + USimpleError::new( + 1, + translate!("od-error-argument-too-large", "option" => "-S", "value" => n.to_string()), + ) + })?), }; let radix = match matches.get_one::(options::ADDRESS_RADIX) { @@ -208,6 +211,7 @@ impl OdOptions { line_bytes, output_duplicates, radix, + string_min_length, }) } } @@ -224,28 +228,39 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let od_options = OdOptions::new(&clap_matches, &args)?; - let mut input_offset = - InputOffset::new(od_options.radix, od_options.skip_bytes, od_options.label); + // Check if we're in strings mode + if let Some(min_length) = od_options.string_min_length { + extract_strings_from_input( + &od_options.input_strings, + od_options.skip_bytes, + od_options.read_bytes, + min_length, + od_options.radix, + ) + } else { + let mut input_offset = + InputOffset::new(od_options.radix, od_options.skip_bytes, od_options.label); - let mut input = open_input_peek_reader( - &od_options.input_strings, - od_options.skip_bytes, - od_options.read_bytes, - ); - let mut input_decoder = InputDecoder::new( - &mut input, - od_options.line_bytes, - PEEK_BUFFER_SIZE, - od_options.byte_order, - ); + let mut input = open_input_peek_reader( + &od_options.input_strings, + od_options.skip_bytes, + od_options.read_bytes, + ); + let mut input_decoder = InputDecoder::new( + &mut input, + od_options.line_bytes, + PEEK_BUFFER_SIZE, + od_options.byte_order, + ); - let output_info = OutputInfo::new( - od_options.line_bytes, - &od_options.formats[..], - od_options.output_duplicates, - ); + let output_info = OutputInfo::new( + od_options.line_bytes, + &od_options.formats[..], + od_options.output_duplicates, + ); - odfunc(&mut input_offset, &mut input_decoder, &output_info) + odfunc(&mut input_offset, &mut input_decoder, &output_info) + } } pub fn uu_app() -> Command { @@ -263,7 +278,7 @@ pub fn uu_app() -> Command { Arg::new(options::HELP) .long(options::HELP) .help(translate!("od-help-help")) - .action(ArgAction::Help) + .action(ArgAction::Help), ) .arg( Arg::new(options::ADDRESS_RADIX) @@ -297,10 +312,8 @@ pub fn uu_app() -> Command { Arg::new(options::STRINGS) .short('S') .long(options::STRINGS) - .help( - "NotImplemented: output strings of at least BYTES graphic chars. 3 is assumed when \ - BYTES is not specified.", - ) + .help(translate!("od-help-strings")) + .num_args(0..=1) .default_missing_value("3") .value_name("BYTES"), ) @@ -337,85 +350,85 @@ pub fn uu_app() -> Command { .arg( Arg::new("o") .short('o') - .help("octal 2-byte units") + .help(translate!("od-help-o")) .action(ArgAction::SetTrue), ) .arg( Arg::new("I") .short('I') - .help("decimal 8-byte units") + .help(translate!("od-help-capital-i")) .action(ArgAction::SetTrue), ) .arg( Arg::new("L") .short('L') - .help("decimal 8-byte units") + .help(translate!("od-help-capital-l")) .action(ArgAction::SetTrue), ) .arg( Arg::new("i") .short('i') - .help("decimal 4-byte units") + .help(translate!("od-help-i")) .action(ArgAction::SetTrue), ) .arg( Arg::new("l") .short('l') - .help("decimal 8-byte units") + .help(translate!("od-help-l")) .action(ArgAction::SetTrue), ) .arg( Arg::new("x") .short('x') - .help("hexadecimal 2-byte units") + .help(translate!("od-help-x")) .action(ArgAction::SetTrue), ) .arg( Arg::new("h") .short('h') - .help("hexadecimal 2-byte units") + .help(translate!("od-help-h")) .action(ArgAction::SetTrue), ) .arg( Arg::new("O") .short('O') - .help("octal 4-byte units") - .action(ArgAction::SetTrue) + .help(translate!("od-help-capital-o")) + .action(ArgAction::SetTrue), ) .arg( Arg::new("s") .short('s') - .help("decimal 2-byte units") + .help(translate!("od-help-s")) .action(ArgAction::SetTrue), ) .arg( Arg::new("X") .short('X') - .help("hexadecimal 4-byte units") + .help(translate!("od-help-capital-x")) .action(ArgAction::SetTrue), ) .arg( Arg::new("H") .short('H') - .help("hexadecimal 4-byte units") + .help(translate!("od-help-capital-h")) .action(ArgAction::SetTrue), ) .arg( Arg::new("e") .short('e') - .help("floating point double precision (64-bit) units") + .help(translate!("od-help-e")) .action(ArgAction::SetTrue), ) .arg( Arg::new("f") .short('f') - .help("floating point double precision (32-bit) units") + .help(translate!("od-help-f")) .action(ArgAction::SetTrue), ) .arg( Arg::new("F") .short('F') - .help("floating point double precision (64-bit) units") + .help(translate!("od-help-capital-f")) .action(ArgAction::SetTrue), ) .arg( @@ -532,6 +545,105 @@ where } } +/// Extract and display printable strings from input (od -S option) +fn extract_strings_from_input( + input_strings: &[String], + skip_bytes: u64, + read_bytes: Option, + min_length: usize, + radix: Radix, +) -> UResult<()> { + let inputs = map_input_strings(input_strings); + let mut mf = MultifileReader::new(inputs); + + // Apply skip_bytes by reading and discarding + let mut skipped = 0u64; + while skipped < skip_bytes { + let to_skip = std::cmp::min(8192, skip_bytes - skipped); + let mut skip_buf = vec![0u8; to_skip as usize]; + match mf.read(&mut skip_buf) { + Ok(0) => break, // EOF reached + Ok(n) => skipped += n as u64, + Err(_) => break, + } + } + + // Helper function to format and print a string + let print_string = |offset: u64, string: &[u8]| { + let string_content = String::from_utf8_lossy(string); + match radix { + Radix::NoPrefix => println!("{string_content}"), + Radix::Decimal => println!("{offset:07} {string_content}"), + Radix::Hexadecimal => println!("{offset:07x} {string_content}"), + Radix::Octal => println!("{offset:07o} {string_content}"), + } + }; + + let mut current_string = Vec::new(); + let mut string_start_offset = 0u64; + let mut current_offset = skip_bytes; + let mut bytes_read = 0u64; + let mut buf = [0u8; 1]; + + loop { + // Check if we've reached the read_bytes limit + if let Some(limit) = read_bytes { + if bytes_read >= limit { + // Special case: when -N limit is reached with a pending string + // that meets min_length, output it even without null terminator + if current_string.len() >= min_length { + print_string(string_start_offset, ¤t_string); + } + break; + } + } + + // Read one byte at a time + match mf.read(&mut buf) { + Ok(0) => break, // EOF + Ok(_) => { + bytes_read += 1; + let byte = buf[0]; + + // Check if it's a printable character (including space) + if (0x20..=0x7E).contains(&byte) { + if current_string.is_empty() { + string_start_offset = current_offset; + } + current_string.push(byte); + } else { + // Either null terminator or non-printable character + if byte == 0 && current_string.len() >= min_length { + // Null terminator found with valid string + print_string(string_start_offset, ¤t_string); + } + current_string.clear(); + } + + current_offset += 1; + } + Err(e) => { + // Note: GNU od does not output unterminated strings at EOF + // Strings must be null-terminated to be output + if mf.has_error() { + show_error!("{}", e); + return Err(1.into()); + } + break; + } + } + } + + // GNU od doesn't output an offset when strings mode finds no valid strings + // This includes cases with only unterminated or too-short strings + + if mf.has_error() { + Err(1.into()) + } else { + Ok(()) + } +} + /// Outputs a single line of input, into one or more lines human readable output. fn print_bytes(prefix: &str, input_decoder: &MemoryDecoder, output_info: &OutputInfo) { let mut first = true; // First line of a multi-format raster. @@ -595,6 +707,17 @@ fn print_bytes(prefix: &str, input_decoder: &MemoryDecoder, output_info: &Output } } +/// Helper function to convert input strings to InputSource +fn map_input_strings(input_strings: &[String]) -> Vec> { + input_strings + .iter() + .map(|w| match w as &str { + "-" => InputSource::Stdin, + x => InputSource::FileName(x), + }) + .collect() +} + /// returns a reader implementing `PeekRead + Read + HasError` providing the combined input /// /// `skip_bytes` is the number of bytes skipped from the input @@ -605,14 +728,7 @@ fn open_input_peek_reader( read_bytes: Option, ) -> PeekReader>>> { // should return "impl PeekRead + Read + HasError" when supported in (stable) rust - let inputs = input_strings - .iter() - .map(|w| match w as &str { - "-" => InputSource::Stdin, - x => InputSource::FileName(x), - }) - .collect::>(); - + let inputs = map_input_strings(input_strings); let mf = MultifileReader::new(inputs); let pr = PartialReader::new(mf, skip_bytes, read_bytes); // Add a BufReader over the top of the PartialReader. This will have the diff --git a/src/uu/sort/benches/sort_locale_bench.rs b/src/uu/sort/benches/sort_locale_bench.rs index e38283560..d6a60effb 100644 --- a/src/uu/sort/benches/sort_locale_bench.rs +++ b/src/uu/sort/benches/sort_locale_bench.rs @@ -14,16 +14,17 @@ use uucore::benchmark::{run_util_function, setup_test_file, text_data}; fn sort_ascii_c_locale(bencher: Bencher) { let data = text_data::generate_ascii_data_simple(100_000); let file_path = setup_test_file(&data); + // Reuse the same output file across iterations to reduce filesystem variance + let output_file = NamedTempFile::new().unwrap(); + let output_path = output_file.path().to_str().unwrap().to_string(); bencher.bench(|| { unsafe { env::set_var("LC_ALL", "C"); } - let output_file = NamedTempFile::new().unwrap(); - let output_path = output_file.path().to_str().unwrap(); black_box(run_util_function( uumain, - &["-o", output_path, file_path.to_str().unwrap()], + &["-o", &output_path, file_path.to_str().unwrap()], )); }); } @@ -33,16 +34,17 @@ fn sort_ascii_c_locale(bencher: Bencher) { fn sort_ascii_utf8_locale(bencher: Bencher) { let data = text_data::generate_ascii_data_simple(200_000); let file_path = setup_test_file(&data); + // Reuse the same output file across iterations to reduce filesystem variance + let output_file = NamedTempFile::new().unwrap(); + let output_path = output_file.path().to_str().unwrap().to_string(); bencher.bench(|| { unsafe { env::set_var("LC_ALL", "en_US.UTF-8"); } - let output_file = NamedTempFile::new().unwrap(); - let output_path = output_file.path().to_str().unwrap(); black_box(run_util_function( uumain, - &["-o", output_path, file_path.to_str().unwrap()], + &["-o", &output_path, file_path.to_str().unwrap()], )); }); } @@ -52,16 +54,17 @@ fn sort_ascii_utf8_locale(bencher: Bencher) { fn sort_mixed_c_locale(bencher: Bencher) { let data = text_data::generate_mixed_locale_data(50_000); let file_path = setup_test_file(&data); + // Reuse the same output file across iterations to reduce filesystem variance + let output_file = NamedTempFile::new().unwrap(); + let output_path = output_file.path().to_str().unwrap().to_string(); bencher.bench(|| { unsafe { env::set_var("LC_ALL", "C"); } - let output_file = NamedTempFile::new().unwrap(); - let output_path = output_file.path().to_str().unwrap(); black_box(run_util_function( uumain, - &["-o", output_path, file_path.to_str().unwrap()], + &["-o", &output_path, file_path.to_str().unwrap()], )); }); } @@ -71,16 +74,17 @@ fn sort_mixed_c_locale(bencher: Bencher) { fn sort_mixed_utf8_locale(bencher: Bencher) { let data = text_data::generate_mixed_locale_data(50_000); let file_path = setup_test_file(&data); + // Reuse the same output file across iterations to reduce filesystem variance + let output_file = NamedTempFile::new().unwrap(); + let output_path = output_file.path().to_str().unwrap().to_string(); bencher.bench(|| { unsafe { env::set_var("LC_ALL", "en_US.UTF-8"); } - let output_file = NamedTempFile::new().unwrap(); - let output_path = output_file.path().to_str().unwrap(); black_box(run_util_function( uumain, - &["-o", output_path, file_path.to_str().unwrap()], + &["-o", &output_path, file_path.to_str().unwrap()], )); }); } @@ -90,16 +94,17 @@ fn sort_mixed_utf8_locale(bencher: Bencher) { fn sort_german_c_locale(bencher: Bencher) { let data = text_data::generate_german_locale_data(50_000); let file_path = setup_test_file(&data); + // Reuse the same output file across iterations to reduce filesystem variance + let output_file = NamedTempFile::new().unwrap(); + let output_path = output_file.path().to_str().unwrap().to_string(); bencher.bench(|| { unsafe { env::set_var("LC_ALL", "C"); } - let output_file = NamedTempFile::new().unwrap(); - let output_path = output_file.path().to_str().unwrap(); black_box(run_util_function( uumain, - &["-o", output_path, file_path.to_str().unwrap()], + &["-o", &output_path, file_path.to_str().unwrap()], )); }); } @@ -109,16 +114,17 @@ fn sort_german_c_locale(bencher: Bencher) { fn sort_german_locale(bencher: Bencher) { let data = text_data::generate_german_locale_data(50_000); let file_path = setup_test_file(&data); + // Reuse the same output file across iterations to reduce filesystem variance + let output_file = NamedTempFile::new().unwrap(); + let output_path = output_file.path().to_str().unwrap().to_string(); bencher.bench(|| { unsafe { env::set_var("LC_ALL", "de_DE.UTF-8"); } - let output_file = NamedTempFile::new().unwrap(); - let output_path = output_file.path().to_str().unwrap(); black_box(run_util_function( uumain, - &["-o", output_path, file_path.to_str().unwrap()], + &["-o", &output_path, file_path.to_str().unwrap()], )); }); } @@ -128,16 +134,17 @@ fn sort_german_locale(bencher: Bencher) { fn sort_random_strings(bencher: Bencher) { let data = text_data::generate_random_strings(50_000, 50); let file_path = setup_test_file(&data); + // Reuse the same output file across iterations to reduce filesystem variance + let output_file = NamedTempFile::new().unwrap(); + let output_path = output_file.path().to_str().unwrap().to_string(); bencher.bench(|| { unsafe { env::set_var("LC_ALL", "en_US.UTF-8"); } - let output_file = NamedTempFile::new().unwrap(); - let output_path = output_file.path().to_str().unwrap(); black_box(run_util_function( uumain, - &["-o", output_path, file_path.to_str().unwrap()], + &["-o", &output_path, file_path.to_str().unwrap()], )); }); } diff --git a/tests/by-util/test_df.rs b/tests/by-util/test_df.rs index b982e929e..149148699 100644 --- a/tests/by-util/test_df.rs +++ b/tests/by-util/test_df.rs @@ -690,6 +690,24 @@ fn test_block_size_from_env() { assert_eq!(get_header("BLOCKSIZE", "333"), "333B-blocks"); } +#[test] +fn test_block_size_from_env_zero() { + fn get_header(env_var: &str, env_value: &str) -> String { + let output = new_ucmd!() + .arg("--output=size") + .env(env_var, env_value) + .succeeds() + .stdout_str_lossy(); + output.lines().next().unwrap().trim().to_string() + } + + let default_block_size_header = "1K-blocks"; + + assert_eq!(get_header("DF_BLOCK_SIZE", "0"), default_block_size_header); + assert_eq!(get_header("BLOCK_SIZE", "0"), default_block_size_header); + assert_eq!(get_header("BLOCKSIZE", "0"), default_block_size_header); +} + #[test] fn test_block_size_from_env_precedences() { fn get_header(one: (&str, &str), two: (&str, &str)) -> String { @@ -747,6 +765,16 @@ fn test_invalid_block_size_from_env() { let header = output.lines().next().unwrap().trim().to_string(); assert_eq!(header, default_block_size_header); + + let output = new_ucmd!() + .arg("--output=size") + .env("DF_BLOCK_SIZE", "0") + .env("BLOCK_SIZE", "222") + .succeeds() + .stdout_str_lossy(); + let header = output.lines().next().unwrap().trim().to_string(); + + assert_eq!(header, default_block_size_header); } #[test] diff --git a/tests/by-util/test_ls.rs b/tests/by-util/test_ls.rs index 46ff99346..a8ce2c32c 100644 --- a/tests/by-util/test_ls.rs +++ b/tests/by-util/test_ls.rs @@ -5355,6 +5355,12 @@ fn test_ls_invalid_block_size() { .fails_with_code(2) .no_stdout() .stderr_is("ls: invalid --block-size argument 'invalid'\n"); + + new_ucmd!() + .arg("--block-size=0") + .fails_with_code(2) + .no_stdout() + .stderr_is("ls: invalid --block-size argument '0'\n"); } #[cfg(all(unix, feature = "dd"))] @@ -5394,6 +5400,14 @@ fn test_ls_invalid_block_size_in_env_var() { .succeeds() .stdout_contains_line("total 4") .stdout_contains(" 1024 "); + + scene + .ucmd() + .arg("-og") + .env("BLOCKSIZE", "0") + .succeeds() + .stdout_contains_line("total 4") + .stdout_contains(" 1024 "); } #[cfg(all(unix, feature = "dd"))] diff --git a/tests/by-util/test_od.rs b/tests/by-util/test_od.rs index 4ee31823e..9ca6a5ebd 100644 --- a/tests/by-util/test_od.rs +++ b/tests/by-util/test_od.rs @@ -3,16 +3,14 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore abcdefghijklmnopqrstuvwxyz Anone fdbb +// spell-checker:ignore abcdefghijklmnopqrstuvwxyz Anone fdbb littl #[cfg(unix)] use std::io::Read; use unindent::unindent; -use uutests::at_and_ucmd; -use uutests::new_ucmd; use uutests::util::TestScenario; -use uutests::util_name; +use uutests::{at_and_ucmd, new_ucmd, util_name}; // octal dump of 'abcdefghijklmnopqrstuvwxyz\n' static ALPHA_OUT: &str = " @@ -32,39 +30,30 @@ fn test_file() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; at.write("test", "abcdefghijklmnopqrstuvwxyz\n"); - scene - .ucmd() - .arg("--endian=little") - .arg("test") - .succeeds() - .no_stderr() - .stdout_is(unindent(ALPHA_OUT)); - scene - .ucmd() - .arg("--endian=littl") // spell-checker:disable-line - .arg("test") - .succeeds() - .no_stderr() - .stdout_is(unindent(ALPHA_OUT)); - scene - .ucmd() - .arg("--endian=l") - .arg("test") - .succeeds() - .no_stderr() - .stdout_is(unindent(ALPHA_OUT)); + + for arg in ["--endian=little", "--endian=littl", "--endian=l"] { + scene + .ucmd() + .arg(arg) + .arg("test") + .succeeds() + .stdout_only(unindent(ALPHA_OUT)); + } + // Ensure that default format matches `-t o2`, and that `-t` does not absorb file argument scene .ucmd() .arg("--endian=little") .arg("-t") .arg("o2") - .arg("test"); + .arg("test") + .succeeds() + .stdout_only(unindent(ALPHA_OUT)); } // Test that od can read 2 files and concatenate the contents #[test] -fn test_2files() { +fn test_two_files() { let (at, mut ucmd) = at_and_ucmd!(); at.write("test1", "abcdefghijklmnop"); at.write("test2", "qrstuvwxyz\n"); // spell-checker:disable-line @@ -72,15 +61,13 @@ fn test_2files() { .arg("test1") .arg("test2") .succeeds() - .no_stderr() - .stdout_is(unindent(ALPHA_OUT)); + .stdout_only(unindent(ALPHA_OUT)); } // Test that od gives non-0 exit val for filename that doesn't exist. #[test] -fn test_no_file() { - let (_at, mut ucmd) = at_and_ucmd!(); - ucmd.arg("}surely'none'would'thus'a'file'name").fails(); +fn test_non_existing_file() { + new_ucmd!().arg("non_existing_file").fails(); } // Test that od reads from stdin instead of a file @@ -91,8 +78,7 @@ fn test_from_stdin() { .arg("--endian=little") .run_piped_stdin(input.as_bytes()) .success() - .no_stderr() - .stdout_is(unindent(ALPHA_OUT)); + .stdout_only(unindent(ALPHA_OUT)); } // Test that od reads from stdin and also from files @@ -110,8 +96,7 @@ fn test_from_mixed() { .arg("test-3") .run_piped_stdin(data2.as_bytes()) .success() - .no_stderr() - .stdout_is(unindent(ALPHA_OUT)); + .stdout_only(unindent(ALPHA_OUT)); } #[test] @@ -122,8 +107,7 @@ fn test_multiple_formats() { .arg("-b") .run_piped_stdin(input.as_bytes()) .success() - .no_stderr() - .stdout_is(unindent( + .stdout_only(unindent( " 0000000 a b c d e f g h i j k l m n o p 141 142 143 144 145 146 147 150 151 152 153 154 155 156 157 160 @@ -151,8 +135,7 @@ fn test_dec() { .arg("-s") .run_piped_stdin(&input[..]) .success() - .no_stderr() - .stdout_is(expected_output); + .stdout_only(expected_output); } #[test] @@ -171,8 +154,7 @@ fn test_hex16() { .arg("-x") .run_piped_stdin(&input[..]) .success() - .no_stderr() - .stdout_is(expected_output); + .stdout_only(expected_output); } #[test] @@ -189,8 +171,7 @@ fn test_hex32() { .arg("-X") .run_piped_stdin(&input[..]) .success() - .no_stderr() - .stdout_is(expected_output); + .stdout_only(expected_output); } #[test] @@ -217,8 +198,7 @@ fn test_f16() { .arg("-w8") .run_piped_stdin(&input[..]) .success() - .no_stderr() - .stdout_is(expected_output); + .stdout_only(expected_output); } #[test] @@ -245,8 +225,7 @@ fn test_fh() { .arg("-w8") .run_piped_stdin(&input[..]) .success() - .no_stderr() - .stdout_is(expected_output); + .stdout_only(expected_output); } #[test] @@ -273,8 +252,7 @@ fn test_fb() { .arg("-w8") .run_piped_stdin(&input[..]) .success() - .no_stderr() - .stdout_is(expected_output); + .stdout_only(expected_output); } #[test] @@ -300,8 +278,7 @@ fn test_f32() { .arg("-f") .run_piped_stdin(&input[..]) .success() - .no_stderr() - .stdout_is(expected_output); + .stdout_only(expected_output); } #[test] @@ -329,8 +306,7 @@ fn test_f64() { .arg("-F") .run_piped_stdin(&input[..]) .success() - .no_stderr() - .stdout_is(expected_output); + .stdout_only(expected_output); } #[test] @@ -340,8 +316,7 @@ fn test_multibyte() { .args(&["-t", "c"]) .run_piped_stdin(input.as_bytes()) .success() - .no_stderr() - .stdout_is(unindent( + .stdout_only(unindent( r" 0000000 342 200 231 342 200 220 313 206 342 200 230 313 234 350 252 236 0000020 360 237 231 202 342 234 205 360 237 220 266 360 235 233 221 U @@ -368,8 +343,7 @@ fn test_width() { .arg("-v") .run_piped_stdin(&input[..]) .success() - .no_stderr() - .stdout_is(expected_output); + .stdout_only(expected_output); } #[test] @@ -425,8 +399,7 @@ fn test_width_without_value() { .arg("-w") .run_piped_stdin(&input[..]) .success() - .no_stderr() - .stdout_is(expected_output); + .stdout_only(expected_output); } #[test] @@ -456,9 +429,8 @@ fn test_suppress_duplicates() { .arg("-O") .arg("-x") .run_piped_stdin(&input[..]) - .no_stderr() .success() - .stdout_is(expected_output); + .stdout_only(expected_output); } #[test] @@ -482,9 +454,8 @@ fn test_big_endian() { .arg("-X") .arg("-x") .run_piped_stdin(&input[..]) - .no_stderr() .success() - .stdout_is(&expected_output); + .stdout_only(&expected_output); new_ucmd!() .arg("--endian=b") .arg("-F") @@ -492,9 +463,8 @@ fn test_big_endian() { .arg("-X") .arg("-x") .run_piped_stdin(&input[..]) - .no_stderr() .success() - .stdout_is(expected_output); + .stdout_only(expected_output); } #[test] @@ -518,9 +488,8 @@ fn test_alignment_Xxa() { .arg("-x") .arg("-a") .run_piped_stdin(&input[..]) - .no_stderr() .success() - .stdout_is(expected_output); + .stdout_only(expected_output); } #[test] @@ -542,9 +511,8 @@ fn test_alignment_Fx() { .arg("-F") .arg("-x") .run_piped_stdin(&input[..]) - .no_stderr() .success() - .stdout_is(expected_output); + .stdout_only(expected_output); } #[test] @@ -570,9 +538,8 @@ fn test_max_uint() { .arg("-Dd") .arg("--format=u1") .run_piped_stdin(&input[..]) - .no_stderr() .success() - .stdout_is(expected_output); + .stdout_only(expected_output); } #[test] @@ -593,9 +560,8 @@ fn test_hex_offset() { .arg("-X") .arg("-X") .run_piped_stdin(&input[..]) - .no_stderr() .success() - .stdout_is(expected_output); + .stdout_only(expected_output); } #[test] @@ -616,9 +582,8 @@ fn test_dec_offset() { .arg("-X") .arg("-X") .run_piped_stdin(&input[..]) - .no_stderr() .success() - .stdout_is(expected_output); + .stdout_only(expected_output); } #[test] @@ -632,9 +597,8 @@ fn test_no_offset() { .arg("-X") .arg("-X") .run_piped_stdin(&input[..]) - .no_stderr() .success() - .stdout_is(expected_output); + .stdout_only(expected_output); } #[test] @@ -670,9 +634,8 @@ fn test_skip_bytes() { .arg("-c") .arg("--skip-bytes=5") .run_piped_stdin(input.as_bytes()) - .no_stderr() .success() - .stdout_is(unindent( + .stdout_only(unindent( " 0000005 f g h i j k l m n o p q 0000021 @@ -687,9 +650,8 @@ fn test_skip_bytes_hex() { .arg("-c") .arg("--skip-bytes=0xB") .run_piped_stdin(input.as_bytes()) - .no_stderr() .success() - .stdout_is(unindent( + .stdout_only(unindent( " 0000013 l m n o p q 0000021 @@ -699,9 +661,8 @@ fn test_skip_bytes_hex() { .arg("-c") .arg("--skip-bytes=0xE") .run_piped_stdin(input.as_bytes()) - .no_stderr() .success() - .stdout_is(unindent( + .stdout_only(unindent( " 0000016 o p q 0000021 @@ -765,9 +726,8 @@ fn test_ascii_dump() { new_ucmd!() .arg("-tx1zacz") // spell-checker:disable-line .run_piped_stdin(&input[..]) - .no_stderr() .success() - .stdout_is(unindent( + .stdout_only(unindent( r" 0000000 00 01 0a 0d 10 1f 20 61 62 63 7d 7e 7f 80 90 a0 >...... abc}~....< nul soh nl cr dle us sp a b c } ~ del nul dle sp @@ -793,8 +753,7 @@ fn test_filename_parsing() { .arg("--") .arg("-f") .succeeds() - .no_stderr() - .stdout_is(unindent( + .stdout_only(unindent( " 000000 m i n u s sp l o w e r c a s e sp 000010 f nl @@ -810,9 +769,8 @@ fn test_stdin_offset() { .arg("-c") .arg("+5") .run_piped_stdin(input.as_bytes()) - .no_stderr() .success() - .stdout_is(unindent( + .stdout_only(unindent( " 0000005 f g h i j k l m n o p q 0000021 @@ -828,8 +786,7 @@ fn test_file_offset() { .arg("-f") .arg("10") .succeeds() - .no_stderr() - .stdout_is(unindent( + .stdout_only(unindent( r" 0000010 w e r c a s e f \n 0000022 @@ -849,9 +806,8 @@ fn test_traditional() { .arg("10") .arg("0") .run_piped_stdin(input.as_bytes()) - .no_stderr() .success() - .stdout_is(unindent( + .stdout_only(unindent( r" 0000010 (0000000) i j k l m n o p q i j k l m n o p q @@ -870,9 +826,8 @@ fn test_traditional_with_skip_bytes_override() { .arg("-c") .arg("0") .run_piped_stdin(input.as_bytes()) - .no_stderr() .success() - .stdout_is(unindent( + .stdout_only(unindent( r" 0000000 a b c d e f g h i j k l m n o p 0000020 @@ -889,9 +844,8 @@ fn test_traditional_with_skip_bytes_non_override() { .arg("--skip-bytes=10") .arg("-c") .run_piped_stdin(input.as_bytes()) - .no_stderr() .success() - .stdout_is(unindent( + .stdout_only(unindent( r" 0000012 k l m n o p 0000020 @@ -923,9 +877,8 @@ fn test_traditional_only_label() { .arg("10") .arg("0x10") .run_piped_stdin(input.as_bytes()) - .no_stderr() .success() - .stdout_is(unindent( + .stdout_only(unindent( r" (0000020) i j k l m n o p q r s t u v w x i j k l m n o p q r s t u v w x @@ -989,6 +942,70 @@ fn test_od_options_after_filename() { .arg("-t") .arg("x2") .succeeds() - .no_stderr() - .stdout_is(" 1c68 fdbb\n"); + .stdout_only(" 1c68 fdbb\n"); +} + +#[test] +fn test_od_strings_option() { + // Test -S option: output strings of at least N graphic chars + + // Test -S0: output all null-terminated strings regardless of length + new_ucmd!() + .arg("-S0") + .pipe_in(b"hello\x00world\x00") + .succeeds() + .stdout_is("0000000 hello\n0000006 world\n"); + + // Test -S0 with single character strings + new_ucmd!() + .arg("-S0") + .pipe_in(b"a\x00b\x00cd\x00") + .succeeds() + .stdout_is("0000000 a\n0000002 b\n0000004 cd\n"); + + // Test with null-terminated strings + new_ucmd!() + .arg("-S3") + .pipe_in(b"\x01hello\x00world\x00ab\x00") + .succeeds() + .stdout_is("0000001 hello\n0000007 world\n"); + + // Test with -S10 to show only strings with minimum length + new_ucmd!() + .arg("-S10") + .pipe_in(b"\x01 \x00 \x00") + .succeeds() + .stdout_is("0000001 \n0000014 \n"); + + // Test with unterminated string at EOF (should not output) + new_ucmd!() + .arg("-S10") + .pipe_in(b" ") + .succeeds() + .no_output(); + + // Test with -N limit and pending string (should output even without null) + let expected = "0000000 \n"; + new_ucmd!() + .arg("-N2") + .arg("-S1") + .pipe_in(" ") + .succeeds() + .stdout_is(expected); + + // Test with -N limit and string too short + new_ucmd!() + .arg("-N11") + .arg("-S11") + .pipe_in(b" \x00") + .succeeds() + .no_output(); + + // Test with no address prefix (-An) + new_ucmd!() + .arg("-S3") + .arg("-An") + .pipe_in(b"hello\x00world\x00") + .succeeds() + .stdout_is("hello\nworld\n"); }