From 84a6e1e1ca76c5f858cdce717442b9a8d102f5f9 Mon Sep 17 00:00:00 2001 From: Etienne Cordonnier Date: Tue, 30 Sep 2025 22:30:53 +0200 Subject: [PATCH 01/15] GNUmakefile: filter out SKIP_UTILS from UTILS This fixes this corner-case where SKIP_UTILS was not taken into account in the logic enabling feat_external_libstdbuf: ``` sudo make install SKIP_UTILS=stdbuf ... error: none of the selected packages contains this feature: feat_external_libstdbuf ``` Signed-off-by: Etienne Cordonnier --- GNUmakefile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/GNUmakefile b/GNUmakefile index 41ba59349..846bc94f5 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -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) From 159f5c8a6eb9c9e386991abaa099483949f5843b Mon Sep 17 00:00:00 2001 From: Etienne Cordonnier Date: Tue, 30 Sep 2025 22:55:37 +0200 Subject: [PATCH 02/15] GNUmakefile: fix installation logic for libstdbuf It is better to check whether we're actually compiling libstdbuf, rather than to check whether we're on windows, in order to decide whether libstdbuf should get installed. Signed-off-by: Etienne Cordonnier --- .github/workflows/CICD.yml | 5 +++++ GNUmakefile | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) 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 846bc94f5..04af26315 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -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 From b1afe3499a7ee3740a309312a29ed0c7f5976ed5 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Wed, 8 Oct 2025 21:08:03 +0200 Subject: [PATCH 03/15] fix tests/od/od-N --- src/uu/od/src/od.rs | 181 +++++++++++++++++++++++++++++++++------ tests/by-util/test_od.rs | 65 ++++++++++++++ 2 files changed, 219 insertions(+), 27 deletions(-) diff --git a/src/uu/od/src/od.rs b/src/uu/od/src/od.rs index e4a8ce108..98373da0f 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,7 @@ struct OdOptions { line_bytes: usize, output_duplicates: bool, radix: Radix, + string_min_length: Option, } impl OdOptions { @@ -169,6 +170,16 @@ impl OdOptions { }, }; + let string_min_length = match parse_bytes_option(matches, options::STRINGS)? { + None => None, + 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) { None => Radix::Octal, Some(s) => { @@ -208,6 +219,7 @@ impl OdOptions { line_bytes, output_duplicates, radix, + string_min_length, }) } } @@ -224,28 +236,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 +286,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 +320,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"), ) @@ -379,8 +400,8 @@ pub fn uu_app() -> Command { .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") @@ -532,6 +553,112 @@ 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 = input_strings + .iter() + .map(|w| match w as &str { + "-" => InputSource::Stdin, + x => InputSource::FileName(x), + }) + .collect::>(); + + 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. diff --git a/tests/by-util/test_od.rs b/tests/by-util/test_od.rs index 4ee31823e..83c864b8e 100644 --- a/tests/by-util/test_od.rs +++ b/tests/by-util/test_od.rs @@ -992,3 +992,68 @@ fn test_od_options_after_filename() { .no_stderr() .stdout_is(" 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() + .stdout_is(""); + + // 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() + .stdout_is(""); + + // Test with no address prefix (-An) + new_ucmd!() + .arg("-S3") + .arg("-An") + .pipe_in(b"hello\x00world\x00") + .succeeds() + .stdout_is("hello\nworld\n"); +} From fd75c372228327e81fc371df050346ad194472f1 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Wed, 8 Oct 2025 22:08:31 +0200 Subject: [PATCH 04/15] od: dedup some code --- src/uu/od/src/od.rs | 69 +++++++++++++++++++-------------------------- 1 file changed, 29 insertions(+), 40 deletions(-) diff --git a/src/uu/od/src/od.rs b/src/uu/od/src/od.rs index 98373da0f..1d4ade4bd 100644 --- a/src/uu/od/src/od.rs +++ b/src/uu/od/src/od.rs @@ -79,6 +79,20 @@ struct OdOptions { 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 { fn new(matches: &ArgMatches, args: &[String]) -> UResult { let byte_order = if let Some(s) = matches.get_one::(options::ENDIAN) { @@ -96,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; @@ -157,18 +160,7 @@ impl OdOptions { let output_duplicates = matches.get_flag(options::OUTPUT_DUPLICATES); - let read_bytes = match matches.get_one::(options::READ_BYTES) { - 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), - )); - } - }, - }; + let read_bytes = parse_bytes_option(matches, options::READ_BYTES)?; let string_min_length = match parse_bytes_option(matches, options::STRINGS)? { None => None, @@ -561,14 +553,7 @@ fn extract_strings_from_input( min_length: usize, radix: Radix, ) -> UResult<()> { - 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 mut mf = MultifileReader::new(inputs); // Apply skip_bytes by reading and discarding @@ -722,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 @@ -732,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 From c1d9fc3c7da6c0ff996535f07e927c75e318bf8e Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Thu, 16 Oct 2025 06:13:54 -0400 Subject: [PATCH 05/15] od: translate some strings --- src/uu/od/locales/en-US.ftl | 15 +++++++++++++++ src/uu/od/locales/fr-FR.ftl | 15 +++++++++++++++ src/uu/od/src/od.rs | 26 +++++++++++++------------- 3 files changed, 43 insertions(+), 13 deletions(-) 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 1d4ade4bd..80e6893d1 100644 --- a/src/uu/od/src/od.rs +++ b/src/uu/od/src/od.rs @@ -350,43 +350,43 @@ 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( @@ -398,37 +398,37 @@ pub fn uu_app() -> Command { .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( From ae5470e7f3141edaf48e0025ae6d80ff0d988d68 Mon Sep 17 00:00:00 2001 From: E <79379754+oech3@users.noreply.github.com> Date: Thu, 16 Oct 2025 22:15:01 +0900 Subject: [PATCH 06/15] move stty to UNIX_PROGS --- GNUmakefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GNUmakefile b/GNUmakefile index 04af26315..9e40cfbdc 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -145,7 +145,6 @@ PROGS := \ sleep \ sort \ split \ - stty \ sum \ sync \ tac \ @@ -185,6 +184,7 @@ UNIX_PROGS := \ pinky \ stat \ stdbuf \ + stty \ timeout \ touch \ tty \ From 9a4f5c4f3d3342d9cb002d9fe6b0a0df1dc8b50f Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Thu, 16 Oct 2025 17:41:32 +0200 Subject: [PATCH 07/15] GNUmakefile: move du & touch to PROGS --- GNUmakefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GNUmakefile b/GNUmakefile index 9e40cfbdc..88ca22120 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -107,6 +107,7 @@ PROGS := \ dir \ dircolors \ dirname \ + du \ echo \ env \ expand \ @@ -151,6 +152,7 @@ PROGS := \ tail \ tee \ test \ + touch \ tr \ true \ truncate \ @@ -168,7 +170,6 @@ UNIX_PROGS := \ chmod \ chown \ chroot \ - du \ groups \ hostid \ hostname \ @@ -186,7 +187,6 @@ UNIX_PROGS := \ stdbuf \ stty \ timeout \ - touch \ tty \ uname \ unlink \ From 11593bd8084219ce360a3b75d8c51c641cb3c0b6 Mon Sep 17 00:00:00 2001 From: naoNao89 <90588855+naoNao89@users.noreply.github.com> Date: Fri, 17 Oct 2025 07:29:53 +0700 Subject: [PATCH 08/15] fuzz: enable debug symbols in release builds Adds debug = true to the [profile.release] section in fuzz/Cargo.toml. This enables generation of backtraces with function names and line numbers when fuzzing discovers crashes, instead of just memory addresses. Fixes #5343 --- fuzz/Cargo.toml | 5 +++++ 1 file changed, 5 insertions(+) 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"] } From 607383a2f9cd0738dabda4d586b7ab0f0dffb851 Mon Sep 17 00:00:00 2001 From: naoNao89 <90588855+naoNao89@users.noreply.github.com> Date: Fri, 17 Oct 2025 09:26:09 +0700 Subject: [PATCH 09/15] bench(sort): reduce variance by reusing output files across iterations Locale benchmarks were creating temp files inside the bench loop (from d1cd9998be), causing filesystem noise and false CodSpeed regressions. The same commit's sort_bench.rs got it right with file creation outside the loop. This fix aligns with that pattern. --- src/uu/sort/benches/sort_locale_bench.rs | 49 ++++++++++++++---------- 1 file changed, 28 insertions(+), 21 deletions(-) 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()], )); }); } From 3ec3849739a8a52bf18223d9833f6e6b50c6d667 Mon Sep 17 00:00:00 2001 From: Christopher Armstrong Date: Fri, 17 Oct 2025 09:29:56 -0400 Subject: [PATCH 10/15] uucore: add parse_size_non_zero_u64 which fails parsing 0 --- src/uucore/src/lib/features/parser/parse_size.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/uucore/src/lib/features/parser/parse_size.rs b/src/uucore/src/lib/features/parser/parse_size.rs index da67a7602..f6f0f1399 100644 --- a/src/uucore/src/lib/features/parser/parse_size.rs +++ b/src/uucore/src/lib/features/parser/parse_size.rs @@ -361,6 +361,15 @@ pub fn parse_size_u64(size: &str) -> Result { Parser::default().parse_u64(size) } +/// Same as `parse_size_u64()`, except 0 fails to parse +pub fn parse_size_non_zero_u64(size: &str) -> Result { + let v = Parser::default().parse_u64(size)?; + if v == 0 { + return Err(ParseSizeError::ParseFailure("0".to_string())); + } + Ok(v) +} + /// Same as `parse_size_u64()` - deprecated #[deprecated = "Please use parse_size_u64(size: &str) -> Result OR parse_size_u128(size: &str) -> Result instead."] pub fn parse_size(size: &str) -> Result { From 6015c8fa312610908d0b6c9052e453f9facdc579 Mon Sep 17 00:00:00 2001 From: Christopher Armstrong Date: Fri, 17 Oct 2025 09:32:37 -0400 Subject: [PATCH 11/15] df: treat env var with zero block size as invalid --- src/uu/df/src/blocks.rs | 4 ++-- tests/by-util/test_df.rs | 28 ++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) 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/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] From 20775fd6a97a4120cbd8c4010cca3c452955d191 Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Fri, 17 Oct 2025 15:48:59 +0200 Subject: [PATCH 12/15] od: fix incomplete test --- tests/by-util/test_od.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/by-util/test_od.rs b/tests/by-util/test_od.rs index 83c864b8e..2fb695b24 100644 --- a/tests/by-util/test_od.rs +++ b/tests/by-util/test_od.rs @@ -53,13 +53,16 @@ fn test_file() { .succeeds() .no_stderr() .stdout_is(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 From 56aad28b0ed6da8277dae97d4d8e7c1811418601 Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Fri, 17 Oct 2025 16:33:49 +0200 Subject: [PATCH 13/15] od: apply small refactorings to tests --- tests/by-util/test_od.rs | 155 +++++++++++++-------------------------- 1 file changed, 52 insertions(+), 103 deletions(-) diff --git a/tests/by-util/test_od.rs b/tests/by-util/test_od.rs index 2fb695b24..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,27 +30,15 @@ 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 @@ -67,7 +53,7 @@ fn test_file() { // 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 @@ -75,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 @@ -94,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 @@ -113,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] @@ -125,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 @@ -154,8 +135,7 @@ fn test_dec() { .arg("-s") .run_piped_stdin(&input[..]) .success() - .no_stderr() - .stdout_is(expected_output); + .stdout_only(expected_output); } #[test] @@ -174,8 +154,7 @@ fn test_hex16() { .arg("-x") .run_piped_stdin(&input[..]) .success() - .no_stderr() - .stdout_is(expected_output); + .stdout_only(expected_output); } #[test] @@ -192,8 +171,7 @@ fn test_hex32() { .arg("-X") .run_piped_stdin(&input[..]) .success() - .no_stderr() - .stdout_is(expected_output); + .stdout_only(expected_output); } #[test] @@ -220,8 +198,7 @@ fn test_f16() { .arg("-w8") .run_piped_stdin(&input[..]) .success() - .no_stderr() - .stdout_is(expected_output); + .stdout_only(expected_output); } #[test] @@ -248,8 +225,7 @@ fn test_fh() { .arg("-w8") .run_piped_stdin(&input[..]) .success() - .no_stderr() - .stdout_is(expected_output); + .stdout_only(expected_output); } #[test] @@ -276,8 +252,7 @@ fn test_fb() { .arg("-w8") .run_piped_stdin(&input[..]) .success() - .no_stderr() - .stdout_is(expected_output); + .stdout_only(expected_output); } #[test] @@ -303,8 +278,7 @@ fn test_f32() { .arg("-f") .run_piped_stdin(&input[..]) .success() - .no_stderr() - .stdout_is(expected_output); + .stdout_only(expected_output); } #[test] @@ -332,8 +306,7 @@ fn test_f64() { .arg("-F") .run_piped_stdin(&input[..]) .success() - .no_stderr() - .stdout_is(expected_output); + .stdout_only(expected_output); } #[test] @@ -343,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 @@ -371,8 +343,7 @@ fn test_width() { .arg("-v") .run_piped_stdin(&input[..]) .success() - .no_stderr() - .stdout_is(expected_output); + .stdout_only(expected_output); } #[test] @@ -428,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] @@ -459,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] @@ -485,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") @@ -495,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] @@ -521,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] @@ -545,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] @@ -573,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] @@ -596,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] @@ -619,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] @@ -635,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] @@ -673,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 @@ -690,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 @@ -702,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 @@ -768,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 @@ -796,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 @@ -813,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 @@ -831,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 @@ -852,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 @@ -873,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 @@ -892,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 @@ -926,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 @@ -992,8 +942,7 @@ fn test_od_options_after_filename() { .arg("-t") .arg("x2") .succeeds() - .no_stderr() - .stdout_is(" 1c68 fdbb\n"); + .stdout_only(" 1c68 fdbb\n"); } #[test] @@ -1033,7 +982,7 @@ fn test_od_strings_option() { .arg("-S10") .pipe_in(b" ") .succeeds() - .stdout_is(""); + .no_output(); // Test with -N limit and pending string (should output even without null) let expected = "0000000 \n"; @@ -1050,7 +999,7 @@ fn test_od_strings_option() { .arg("-S11") .pipe_in(b" \x00") .succeeds() - .stdout_is(""); + .no_output(); // Test with no address prefix (-An) new_ucmd!() From 3ff51d6402a2d8f9cf40d49b89ade859983a060b Mon Sep 17 00:00:00 2001 From: Zackary Ayoun Date: Thu, 16 Oct 2025 17:02:26 +0000 Subject: [PATCH 14/15] ls: fix zero block size handling to match GNU ls - Reject --block-size=0 with "invalid --block-size argument '0'" error using parse_size_non_zero_u64 - Add test coverage for both command-line and env var cases Matches GNU ls behavior where command-line zero is invalid but environment variable zero is silently ignored. --- src/uu/ls/src/ls.rs | 4 ++-- tests/by-util/test_ls.rs | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) 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/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"))] From 7ea323cf73f4d19a24b988c58f821c19cdb0f312 Mon Sep 17 00:00:00 2001 From: E <79379754+oech3@users.noreply.github.com> Date: Sat, 18 Oct 2025 17:37:17 +0900 Subject: [PATCH 15/15] Buind {host,u}name on Windows They were buildable on MSYS2. --- GNUmakefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GNUmakefile b/GNUmakefile index 9e40cfbdc..ba306fa3a 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -117,6 +117,7 @@ PROGS := \ fold \ hashsum \ head \ + hostname \ join \ link \ ln \ @@ -155,6 +156,7 @@ PROGS := \ true \ truncate \ tsort \ + uname \ unexpand \ uniq \ vdir \ @@ -171,7 +173,6 @@ UNIX_PROGS := \ du \ groups \ hostid \ - hostname \ id \ install \ kill \ @@ -188,7 +189,6 @@ UNIX_PROGS := \ timeout \ touch \ tty \ - uname \ unlink \ uptime \ users \