From 5fde2f410d29daa058eeb1ea311987eac14d7193 Mon Sep 17 00:00:00 2001 From: Kevin Burke Date: Thu, 19 Mar 2026 22:32:55 -0700 Subject: [PATCH 001/264] rm: use "traversal failed" message when readdir fails during recursive removal When readdir fails mid-traversal (e.g., due to I/O errors), GNU rm reports "traversal failed: " rather than "cannot remove ''". Update the error handling in safe_remove_dir_recursive_impl to match this behavior. Fixes GNU test: tests/rm/rm-readdir-fail.sh Co-Authored-By: Claude Opus 4.6 --- src/uu/rm/locales/en-US.ftl | 1 + src/uu/rm/locales/fr-FR.ftl | 1 + src/uu/rm/src/platform/unix.rs | 6 +++++- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/uu/rm/locales/en-US.ftl b/src/uu/rm/locales/en-US.ftl index 8a9a3513eaa..2b7a570fd42 100644 --- a/src/uu/rm/locales/en-US.ftl +++ b/src/uu/rm/locales/en-US.ftl @@ -44,6 +44,7 @@ rm-error-dangerous-recursive-operation-same-as-root = it is dangerous to operate rm-error-use-no-preserve-root = use --no-preserve-root to override this failsafe rm-error-refusing-to-remove-directory = refusing to remove '.' or '..' directory: skipping {$path} rm-error-cannot-remove = cannot remove {$file} +rm-error-traversal-failed = traversal failed: {$path} rm-error-may-not-abbreviate-no-preserve-root = you may not abbreviate the --no-preserve-root option # Verbose messages diff --git a/src/uu/rm/locales/fr-FR.ftl b/src/uu/rm/locales/fr-FR.ftl index 6052e2f8964..9c9124db676 100644 --- a/src/uu/rm/locales/fr-FR.ftl +++ b/src/uu/rm/locales/fr-FR.ftl @@ -44,6 +44,7 @@ rm-error-dangerous-recursive-operation-same-as-root = il est dangereux d'opérer rm-error-use-no-preserve-root = utilisez --no-preserve-root pour outrepasser cette protection rm-error-refusing-to-remove-directory = refus de supprimer le répertoire '.' ou '..' : ignorer {$path} rm-error-cannot-remove = impossible de supprimer {$file} +rm-error-traversal-failed = échec du parcours : {$path} rm-error-may-not-abbreviate-no-preserve-root = Vous ne pouvez pas abréger l'option --no-preserve-root # Messages verbeux diff --git a/src/uu/rm/src/platform/unix.rs b/src/uu/rm/src/platform/unix.rs index d2bcf6e2f46..43ff01dd34b 100644 --- a/src/uu/rm/src/platform/unix.rs +++ b/src/uu/rm/src/platform/unix.rs @@ -349,7 +349,11 @@ pub fn safe_remove_dir_recursive_impl(path: &Path, dir_fd: &DirFd, options: &Opt return !options.force; } Err(e) => { - return handle_error_with_force(e, path, options); + let e = e.map_err_context(|| { + translate!("rm-error-traversal-failed", "path" => path.display().to_string()) + }); + show_error!("{e}"); + return true; } }; From ed919a4368368ce33d496742d38860848db85141 Mon Sep 17 00:00:00 2001 From: Kevin Burke Date: Fri, 20 Mar 2026 09:43:35 -0700 Subject: [PATCH 002/264] rm: fix cargo fmt style violation in map_err_context closure Co-Authored-By: Claude Opus 4.6 --- src/uu/rm/src/platform/unix.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/uu/rm/src/platform/unix.rs b/src/uu/rm/src/platform/unix.rs index 43ff01dd34b..08ae94fdd23 100644 --- a/src/uu/rm/src/platform/unix.rs +++ b/src/uu/rm/src/platform/unix.rs @@ -349,9 +349,9 @@ pub fn safe_remove_dir_recursive_impl(path: &Path, dir_fd: &DirFd, options: &Opt return !options.force; } Err(e) => { - let e = e.map_err_context(|| { - translate!("rm-error-traversal-failed", "path" => path.display().to_string()) - }); + let e = e.map_err_context( + || translate!("rm-error-traversal-failed", "path" => path.display().to_string()), + ); show_error!("{e}"); return true; } From 3aebc546a7c5ce8a0f8d14f7a8c35dbd8c7b4c3f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 04:46:42 +0000 Subject: [PATCH 003/264] chore(deps): update nick-fields/retry action to v4 --- .github/workflows/code-quality.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index adbd1ec268b..e5f47562d6a 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -116,7 +116,7 @@ jobs: ;; esac - name: "`cargo clippy` lint testing" - uses: nick-fields/retry@v3 + uses: nick-fields/retry@v4 with: max_attempts: 3 retry_on: error From 0e9ea0b82a354cb376c32667246097dce8a7d715 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 16:49:44 +0000 Subject: [PATCH 004/264] chore(deps): update rust crate zip to v8.3.0 --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a34facd0566..d02243318c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5169,9 +5169,9 @@ dependencies = [ [[package]] name = "zip" -version = "8.2.0" +version = "8.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b680f2a0cd479b4cff6e1233c483fdead418106eae419dc60200ae9850f6d004" +checksum = "4a243cfad17427fc077f529da5a95abe4e94fd2bfdb601611870a6557cc67657" dependencies = [ "crc32fast", "flate2", From b4e3fb1155f432378b9bf6726ff0bf0c4f7c9714 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 01:20:38 +0000 Subject: [PATCH 005/264] chore(deps): update rust crate itoa to v1.0.18 --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d02243318c1..6237a36609f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1692,9 +1692,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" From c5f48bbd5fb737b896e0fceaefeb44e572c900bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Can=20B=C3=B6l=C3=BCk?= Date: Fri, 20 Mar 2026 10:30:19 +0100 Subject: [PATCH 006/264] cut: improve function signature and add test for existing functionality --- src/uu/cut/src/cut.rs | 4 ++-- tests/by-util/test_cut.rs | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/uu/cut/src/cut.rs b/src/uu/cut/src/cut.rs index 714e423e8d5..e0568e810e4 100644 --- a/src/uu/cut/src/cut.rs +++ b/src/uu/cut/src/cut.rs @@ -260,9 +260,9 @@ fn cut_fields_newline_char_delim( reader: R, out: &mut W, ranges: &[Range], - only_delimited: bool, newline_char: u8, out_delim: &[u8], + only_delimited: bool, ) -> UResult<()> { let mut reader = BufReader::new(reader); let mut line = Vec::new(); @@ -398,9 +398,9 @@ fn cut_fields( reader, out, ranges, - field_opts.only_delimited, newline_char, out_delim, + field_opts.only_delimited, ) } Delimiter::Slice(delim) => { diff --git a/tests/by-util/test_cut.rs b/tests/by-util/test_cut.rs index abb817b1400..160ad59c878 100644 --- a/tests/by-util/test_cut.rs +++ b/tests/by-util/test_cut.rs @@ -229,6 +229,21 @@ fn test_zero_terminated_only_delimited() { .stdout_only("82\n7\0"); } +#[test] +fn test_suppresses_unterminated_segment() { + new_ucmd!() + .args(&["-z", "-d", "", "-s", "-f", "1"]) + .pipe_in("unterminated") + .succeeds() + .stdout_only_bytes(""); + + new_ucmd!() + .args(&["-z", "-d", "", "-s", "-f", "1"]) + .pipe_in("terminated\0unterminated") + .succeeds() + .stdout_only_bytes("terminated\0"); +} + #[test] fn test_is_a_directory() { let (at, mut ucmd) = at_and_ucmd!(); From c51291a656863a942e92f38aa039df4faeafa1f6 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Wed, 18 Mar 2026 23:39:25 +0100 Subject: [PATCH 007/264] uucore: add compute_sort_key_utf8 for pre-computing ICU collation keys Expose ICU4X's write_sort_key_utf8_to via a public helper that appends a collation sort key to a caller-supplied buffer. This enables computing sort keys once per line (O(n)) and then comparing cheap byte arrays during sorting, instead of calling compare_utf8 on every comparison. --- src/uucore/src/lib/features/i18n/collator.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/uucore/src/lib/features/i18n/collator.rs b/src/uucore/src/lib/features/i18n/collator.rs index 007e17c901b..9a8ee1fc649 100644 --- a/src/uucore/src/lib/features/i18n/collator.rs +++ b/src/uucore/src/lib/features/i18n/collator.rs @@ -74,6 +74,17 @@ pub fn init_locale_collation() -> bool { try_init_collator(opts) } +/// Compute the ICU collation sort key for the given input bytes and append it to `buf`. +/// This allows pre-computing sort keys once per line, then comparing them with simple +/// byte comparison during sorting (much faster than calling `compare_utf8` per comparison). +pub fn compute_sort_key_utf8(input: &[u8], buf: &mut Vec) { + let c = COLLATOR + .get() + .expect("compute_sort_key_utf8 called before collator initialization"); + c.write_sort_key_utf8_to(input, buf) + .expect("ICU write_sort_key_utf8_to failed"); +} + /// Compare both strings with regard to the current locale. pub fn locale_cmp(left: &[u8], right: &[u8]) -> Ordering { // If the detected locale is 'C', just do byte-wise comparison From 063404eedd9d7abf284cad75949e62a54e6417e1 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Wed, 18 Mar 2026 23:40:54 +0100 Subject: [PATCH 008/264] sort: add collation key arena storage to LineData Add collation_key_buffer (arena) and collation_key_ends (offsets) to LineData and RecycledChunk, with a collation_key() accessor. All sort keys for a chunk are stored in a single Vec to avoid millions of small heap allocations. --- src/uu/sort/src/chunks.rs | 57 ++++++++++++++++++++++++++++----------- 1 file changed, 41 insertions(+), 16 deletions(-) diff --git a/src/uu/sort/src/chunks.rs b/src/uu/sort/src/chunks.rs index dc1fab07033..1e0c89d9636 100644 --- a/src/uu/sort/src/chunks.rs +++ b/src/uu/sort/src/chunks.rs @@ -53,6 +53,23 @@ pub struct LineData<'a> { pub num_infos: Vec, pub parsed_floats: Vec, pub line_num_floats: Vec>, + /// Arena buffer holding all collation sort keys concatenated. + pub collation_key_buffer: Vec, + /// End offsets into `collation_key_buffer` for each line's sort key. + pub collation_key_ends: Vec, +} + +impl LineData<'_> { + /// Get the collation sort key for a line at the given index. + pub fn collation_key(&self, index: usize) -> &[u8] { + let start = if index == 0 { + 0 + } else { + self.collation_key_ends[index - 1] + }; + let end = self.collation_key_ends[index]; + &self.collation_key_buffer[start..end] + } } impl Chunk { @@ -64,6 +81,8 @@ impl Chunk { contents.line_data.num_infos.clear(); contents.line_data.parsed_floats.clear(); contents.line_data.line_num_floats.clear(); + contents.line_data.collation_key_buffer.clear(); + contents.line_data.collation_key_ends.clear(); contents.token_buffer.clear(); let lines = unsafe { // SAFETY: It is safe to (temporarily) transmute to a vector of lines with a longer lifetime, @@ -84,23 +103,19 @@ impl Chunk { ( lines, selections, - std::mem::take(&mut contents.line_data.num_infos), - std::mem::take(&mut contents.line_data.parsed_floats), - std::mem::take(&mut contents.line_data.line_num_floats), - std::mem::take(&mut contents.token_buffer), - contents.line_count_hint, - ) + num_infos: std::mem::take(&mut contents.line_data.num_infos), + parsed_floats: std::mem::take(&mut contents.line_data.parsed_floats), + line_num_floats: std::mem::take(&mut contents.line_data.line_num_floats), + collation_key_buffer: std::mem::take(&mut contents.line_data.collation_key_buffer), + collation_key_ends: std::mem::take(&mut contents.line_data.collation_key_ends), + token_buffer: std::mem::take(&mut contents.token_buffer), + line_count_hint: contents.line_count_hint, + // buffer is set below after we consume `self` + buffer: Vec::new(), + } }); - RecycledChunk { - lines: recycled_contents.0, - selections: recycled_contents.1, - num_infos: recycled_contents.2, - parsed_floats: recycled_contents.3, - line_num_floats: recycled_contents.4, - token_buffer: recycled_contents.5, - line_count_hint: recycled_contents.6, - buffer: self.into_owner(), - } + recycled_contents.buffer = self.into_owner(); + recycled_contents } pub fn lines(&self) -> &Vec> { @@ -118,6 +133,8 @@ pub struct RecycledChunk { num_infos: Vec, parsed_floats: Vec, line_num_floats: Vec>, + collation_key_buffer: Vec, + collation_key_ends: Vec, token_buffer: Vec>, line_count_hint: usize, buffer: Vec, @@ -131,6 +148,8 @@ impl RecycledChunk { num_infos: Vec::new(), parsed_floats: Vec::new(), line_num_floats: Vec::new(), + collation_key_buffer: Vec::new(), + collation_key_ends: Vec::new(), token_buffer: Vec::new(), line_count_hint: 0, buffer: vec![0; capacity], @@ -176,6 +195,8 @@ pub fn read( num_infos, parsed_floats, line_num_floats, + collation_key_buffer, + collation_key_ends, mut token_buffer, mut line_count_hint, mut buffer, @@ -214,6 +235,8 @@ pub fn read( num_infos, parsed_floats, line_num_floats, + collation_key_buffer, + collation_key_ends, }; parse_lines( read, @@ -253,6 +276,8 @@ fn parse_lines<'a>( assert!(line_data.num_infos.is_empty()); assert!(line_data.parsed_floats.is_empty()); assert!(line_data.line_num_floats.is_empty()); + assert!(line_data.collation_key_buffer.is_empty()); + assert!(line_data.collation_key_ends.is_empty()); token_buffer.clear(); const SMALL_CHUNK_BYTES: usize = 64 * 1024; let mut estimated = (*line_count_hint).max(1); From 93ce94f9e2f160432c92b1319c352bdef3e220a3 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Wed, 18 Mar 2026 23:41:15 +0100 Subject: [PATCH 009/264] sort: pre-compute ICU collation sort keys for fast locale sorting Add a fast_locale_collation path that pre-computes ICU sort keys during line parsing (O(n)), then uses cheap byte-array comparison during sorting (O(n log n)). This replaces per-comparison ICU compare_utf8 calls for the common case of plain `sort` with a UTF-8 locale. With 1M lines and LC_ALL=en_US.UTF-8, this is ~2.6x faster than GNU sort (314ms vs 822ms). --- src/uu/sort/src/chunks.rs | 4 ++-- src/uu/sort/src/sort.rs | 26 +++++++++++++++++++++++++- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/uu/sort/src/chunks.rs b/src/uu/sort/src/chunks.rs index 1e0c89d9636..c808c622b81 100644 --- a/src/uu/sort/src/chunks.rs +++ b/src/uu/sort/src/chunks.rs @@ -75,7 +75,7 @@ impl LineData<'_> { impl Chunk { /// Destroy this chunk and return its components to be reused. pub fn recycle(mut self) -> RecycledChunk { - let recycled_contents = self.with_dependent_mut(|_, contents| { + let mut recycled_contents = self.with_dependent_mut(|_, contents| { contents.lines.clear(); contents.line_data.selections.clear(); contents.line_data.num_infos.clear(); @@ -100,7 +100,7 @@ impl Chunk { &mut contents.line_data.selections, )) }; - ( + RecycledChunk { lines, selections, num_infos: std::mem::take(&mut contents.line_data.num_infos), diff --git a/src/uu/sort/src/sort.rs b/src/uu/sort/src/sort.rs index 52d40511198..c87025a509d 100644 --- a/src/uu/sort/src/sort.rs +++ b/src/uu/sort/src/sort.rs @@ -48,7 +48,7 @@ use uucore::error::{FromIo, strip_errno}; use uucore::error::{UError, UResult, USimpleError, UUsageError}; use uucore::extendedbigdecimal::ExtendedBigDecimal; #[cfg(feature = "i18n-collator")] -use uucore::i18n::collator::locale_cmp; +use uucore::i18n::collator::{compute_sort_key_utf8, locale_cmp}; use uucore::i18n::decimal::locale_decimal_separator; use uucore::line_ending::LineEnding; use uucore::parser::num_parser::{ExtendedParser, ExtendedParserError}; @@ -324,6 +324,7 @@ struct Precomputed { floats_per_line: usize, selections_per_line: usize, fast_lexicographic: bool, + fast_locale_collation: bool, fast_ascii_insensitive: bool, tokenize_blank_thousands_sep: bool, tokenize_allow_unit_after_blank: bool, @@ -387,6 +388,8 @@ impl GlobalSettings { self.precomputed.fast_lexicographic = !disable_fast_lexicographic && self.can_use_fast_lexicographic(); + self.precomputed.fast_locale_collation = + disable_fast_lexicographic && self.can_use_fast_lexicographic(); self.precomputed.fast_ascii_insensitive = self.can_use_fast_ascii_insensitive(); } @@ -632,6 +635,15 @@ impl<'a> Line<'a> { token_buffer: &mut Vec, settings: &GlobalSettings, ) -> Self { + #[cfg(feature = "i18n-collator")] + if settings.precomputed.fast_locale_collation { + compute_sort_key_utf8(line, &mut line_data.collation_key_buffer); + line_data + .collation_key_ends + .push(line_data.collation_key_buffer.len()); + return Self { line, index }; + } + let needs_line_data = settings.precomputed.needs_tokens || settings.precomputed.selections_per_line > 0 || settings.precomputed.num_infos_per_line > 0 @@ -2614,6 +2626,18 @@ fn compare_by<'a>( }; } + #[cfg(feature = "i18n-collator")] + if global_settings.precomputed.fast_locale_collation { + let a_key = a_line_data.collation_key(a.index); + let b_key = b_line_data.collation_key(b.index); + let cmp = a_key.cmp(b_key); + return if global_settings.reverse { + cmp.reverse() + } else { + cmp + }; + } + if global_settings.precomputed.fast_ascii_insensitive { let cmp = ascii_case_insensitive_cmp(a.line, b.line); if cmp != Ordering::Equal || a.line == b.line { From e6e1e9ce441db3a498f597789d45352b60b38d1c Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Tue, 10 Mar 2026 03:26:18 +0900 Subject: [PATCH 010/264] GNUmakefile: Swap uname -s with rustc --print host-tuple for cross-build and MSYS --- .github/workflows/make.yml | 34 ++++++++++++++++++++++++++++++++++ GNUmakefile | 19 ++++++++----------- 2 files changed, 42 insertions(+), 11 deletions(-) diff --git a/.github/workflows/make.yml b/.github/workflows/make.yml index 7fc96461216..fb1ed4bd9c6 100644 --- a/.github/workflows/make.yml +++ b/.github/workflows/make.yml @@ -266,6 +266,40 @@ jobs: # 2. the makefile doesn't try to install libstdbuf even though stdbuf is skipped DESTDIR=/tmp/ make SKIP_UTILS="stdbuf" install + # keep this job minimal to avoid have many duplicated build with CICD + build_makefile-other: + name: Build/Makefile + runs-on: ${{ matrix.job.os }} + env: + CARGO_INCREMENTAL: 0 + strategy: + fail-fast: false + matrix: + job: + - { os: windows-latest , features: feat_os_windows } + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + - uses: Swatinem/rust-cache@v2 + - name: Run sccache-cache + id: sccache-setup + uses: mozilla-actions/sccache-action@v0.0.9 + continue-on-error: true + - name: Export sccache + if: steps.sccache-setup.outcome == 'success' + run: | + echo "RUSTC_WRAPPER=sccache" >> $GITHUB_ENV + echo "SCCACHE_GHA_ENABLED=true" >> $GITHUB_ENV + - name: "`make build`" + shell: bash + run: | + set -x + # Check that we exclude unix programs to avoid build failure + make PREFIX=/tmp/usr MULTICALL=y COMPLETIONS=n MANPAGES=n LOCALES=n \ + SKIP_UTILS="arch b2sum base32 base64 basename basenc cat cksum comm cp csplit cut date dd df dir dircolors dirname du echo env expand expr factor false fmt fold head hostname join link ln ls md5sum mkdir mktemp more mv nl nproc numfmt od paste pr printenv printf ptx pwd readlink realpath rm rmdir seq sha1sum sha224sum sha256sum sha384sum sha512sum shred shuf sleep sort split sum sync tac tail tee test touch tr truncate tsort uname unexpand uniq unlink vdir wc whoami yes" + target/debug/coreutils.exe true + test_busybox: name: Tests/BusyBox test suite runs-on: ${{ matrix.job.os }} diff --git a/GNUmakefile b/GNUmakefile index ae266b6c277..d3cfa2f6464 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -62,17 +62,14 @@ TOYBOX_ROOT := $(BASEDIR)/tmp TOYBOX_VER := 0.8.12 TOYBOX_SRC := $(TOYBOX_ROOT)/toybox-$(TOYBOX_VER) -#------------------------------------------------------------------------ -# Detect the host system. -# On Windows uname -s might return MINGW_NT-* or CYGWIN_NT-*. -# Otherwise let it default to the kernel name returned by uname -s -# (Linux, Darwin, FreeBSD, …). -#------------------------------------------------------------------------ -OS ?= $(shell uname -s) +# Detect the target system +# See https://doc.rust-lang.org/beta/rustc/platform-support.html +# todo: support building wasm +OS := $(or $(CARGO_BUILD_TARGET),$(shell rustc --print host-tuple)) # Windows does not allow symlink by default. # Allow to override LN for AppArmor. -ifneq (,$(findstring _NT,$(OS))) +ifneq (,$(findstring windows,$(OS))) LN ?= ln -f endif LN ?= ln -sf @@ -97,7 +94,7 @@ SELINUX_PROGS := \ $(info Detected OS = $(OS)) -ifeq (,$(findstring MINGW,$(OS))) +ifeq (,$(findstring windows,$(OS))) PROGS += $(UNIX_PROGS) endif ifeq ($(SELINUX_ENABLED),1) @@ -288,7 +285,7 @@ install: build install-manpages install-completions install-locales mkdir -p $(INSTALLDIR_BIN) ifneq (,$(and $(findstring stdbuf,$(UTILS)),$(findstring feat_external_libstdbuf,$(CARGOFLAGS)))) mkdir -p $(DESTDIR)$(LIBSTDBUF_DIR) -ifneq (,$(findstring CYGWIN,$(OS))) +ifneq (,$(findstring cygwin,$(OS))) $(INSTALL) -m 755 $(BUILDDIR)/deps/stdbuf.dll $(DESTDIR)$(LIBSTDBUF_DIR)/libstdbuf.dll else $(INSTALL) -m 755 $(BUILDDIR)/deps/libstdbuf.* $(DESTDIR)$(LIBSTDBUF_DIR)/ @@ -308,7 +305,7 @@ else endif uninstall: -ifeq (,$(findstring MINGW,$(OS))) +ifeq (,$(findstring windows,$(OS))) rm -f $(DESTDIR)$(LIBSTDBUF_DIR)/libstdbuf.* -rm -d $(DESTDIR)$(LIBSTDBUF_DIR) 2>/dev/null || true endif From a98fd89835fd7c1c24a0f75eabfa310af6c7645a Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Mon, 16 Mar 2026 19:52:19 +0900 Subject: [PATCH 011/264] GNUmakefile: Do not depend on system tr for Gentoo --- GNUmakefile | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/GNUmakefile b/GNUmakefile index d3cfa2f6464..a99fefa46ec 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -74,20 +74,6 @@ ifneq (,$(findstring windows,$(OS))) endif LN ?= ln -sf -# Possible programs -PROGS := \ - $(shell sed -n '/feat_Tier1 = \[/,/\]/p' Cargo.toml | sed '1d;2d' |tr -d '],"\n')\ - $(shell sed -n '/feat_common_core = \[/,/\]/p' Cargo.toml | sed '1d' |tr -d '],"\n') - -UNIX_PROGS := \ - $(shell sed -n '/feat_require_unix_core = \[/,/\]/p' Cargo.toml | sed '1d' |tr -d '],"\n') \ - hostid \ - pinky \ - stdbuf \ - uptime \ - users \ - who - SELINUX_PROGS := \ chcon \ runcon @@ -95,8 +81,12 @@ SELINUX_PROGS := \ $(info Detected OS = $(OS)) ifeq (,$(findstring windows,$(OS))) - PROGS += $(UNIX_PROGS) + FEATURE_EXTRACT_UTILS := feat_os_unix +else + FEATURE_EXTRACT_UTILS := feat_Tier1 endif +PROGS := $(shell cargo tree --depth 1 --features $(FEATURE_EXTRACT_UTILS) --format "{p}" --prefix none | sed -E -n 's/^uu_([^ ]+).*/\1/p') + ifeq ($(SELINUX_ENABLED),1) PROGS += $(SELINUX_PROGS) endif @@ -111,7 +101,7 @@ endif # Programs with usable tests TESTS := \ - $(sort $(filter $(UTILS),$(PROGS) $(UNIX_PROGS) $(SELINUX_PROGS))) + $(sort $(filter $(UTILS),$(PROGS) $(SELINUX_PROGS))) TEST_NO_FAIL_FAST := TEST_SPEC_FEATURE := From 0fc571c69465ba0cc7c2f5536e3ba4368c049554 Mon Sep 17 00:00:00 2001 From: "Guillem L. Jara" <4lon3ly0@tutanota.com> Date: Fri, 20 Mar 2026 10:24:14 +0100 Subject: [PATCH 012/264] chore(ls): refactor into files Splits the 4000 LOC ls.rs codebase into three of ~1000-1500 each. Makes the codebase more manageable. --- src/uu/ls/src/config.rs | 1085 +++++++++++++++ src/uu/ls/src/display.rs | 1314 ++++++++++++++++++ src/uu/ls/src/ls.rs | 2736 +++----------------------------------- 3 files changed, 2596 insertions(+), 2539 deletions(-) create mode 100644 src/uu/ls/src/config.rs create mode 100644 src/uu/ls/src/display.rs diff --git a/src/uu/ls/src/config.rs b/src/uu/ls/src/config.rs new file mode 100644 index 00000000000..035b83fd779 --- /dev/null +++ b/src/uu/ls/src/config.rs @@ -0,0 +1,1085 @@ +// spell-checker:ignore (ToDO) somegroup nlink tabsize dired subdired dtype colorterm stringly +// spell-checker:ignore nohash strtime clocale + +use std::{ + ffi::{OsStr, OsString}, + io::{IsTerminal, stdout}, + num::IntErrorKind, +}; + +use glob::Pattern; +use lscolors::LsColors; +use term_grid::SPACES_IN_TAB; + +use uucore::{ + display::Quotable, error::UResult, format::human::SizeFormat, fsext::MetadataTimeField, + line_ending::LineEnding, parser::parse_glob, parser::parse_size::parse_size_non_zero_u64, + quoting_style::QuotingStyle, show_error, show_warning, time::format, translate, +}; + +use crate::{ + LsError, + colors::{LsColorsParseError, validate_ls_colors_env}, + dired::is_dired_arg_present, + display::{Format, IndicatorStyle, LocaleQuoting, LongFormat}, + options::QUOTING_STYLE, +}; + +pub mod options { + pub mod format { + pub static ONE_LINE: &str = "1"; + pub static LONG: &str = "long"; + pub static COLUMNS: &str = "C"; + pub static ACROSS: &str = "x"; + pub static TAB_SIZE: &str = "tabsize"; + pub static COMMAS: &str = "m"; + pub static LONG_NO_OWNER: &str = "g"; + pub static LONG_NO_GROUP: &str = "o"; + pub static LONG_NUMERIC_UID_GID: &str = "numeric-uid-gid"; + } + + pub mod files { + pub static ALL: &str = "all"; + pub static ALMOST_ALL: &str = "almost-all"; + pub static UNSORTED_ALL: &str = "f"; + } + + pub mod sort { + pub static SIZE: &str = "S"; + pub static TIME: &str = "t"; + pub static NONE: &str = "U"; + pub static VERSION: &str = "v"; + pub static EXTENSION: &str = "X"; + } + + pub mod time { + pub static ACCESS: &str = "u"; + pub static CHANGE: &str = "c"; + } + + pub mod size { + pub static ALLOCATION_SIZE: &str = "size"; + pub static BLOCK_SIZE: &str = "block-size"; + pub static HUMAN_READABLE: &str = "human-readable"; + pub static SI: &str = "si"; + pub static KIBIBYTES: &str = "kibibytes"; + } + + pub mod quoting { + pub static ESCAPE: &str = "escape"; + pub static LITERAL: &str = "literal"; + pub static C: &str = "quote-name"; + } + + pub mod indicator_style { + pub static SLASH: &str = "p"; + pub static FILE_TYPE: &str = "file-type"; + pub static CLASSIFY: &str = "classify"; + } + + pub mod dereference { + pub static ALL: &str = "dereference"; + pub static ARGS: &str = "dereference-command-line"; + pub static DIR_ARGS: &str = "dereference-command-line-symlink-to-dir"; + } + + pub static HELP: &str = "help"; + pub static QUOTING_STYLE: &str = "quoting-style"; + pub static HIDE_CONTROL_CHARS: &str = "hide-control-chars"; + pub static SHOW_CONTROL_CHARS: &str = "show-control-chars"; + pub static WIDTH: &str = "width"; + pub static AUTHOR: &str = "author"; + pub static NO_GROUP: &str = "no-group"; + pub static FORMAT: &str = "format"; + pub static SORT: &str = "sort"; + pub static TIME: &str = "time"; + pub static IGNORE_BACKUPS: &str = "ignore-backups"; + pub static DIRECTORY: &str = "directory"; + pub static INODE: &str = "inode"; + pub static REVERSE: &str = "reverse"; + pub static RECURSIVE: &str = "recursive"; + pub static COLOR: &str = "color"; + pub static PATHS: &str = "paths"; + pub static INDICATOR_STYLE: &str = "indicator-style"; + pub static TIME_STYLE: &str = "time-style"; + pub static FULL_TIME: &str = "full-time"; + pub static HIDE: &str = "hide"; + pub static IGNORE: &str = "ignore"; + pub static CONTEXT: &str = "context"; + pub static GROUP_DIRECTORIES_FIRST: &str = "group-directories-first"; + pub static ZERO: &str = "zero"; + pub static DIRED: &str = "dired"; + pub static HYPERLINK: &str = "hyperlink"; +} + +const DEFAULT_TERM_WIDTH: u16 = 80; +const POSIXLY_CORRECT_BLOCK_SIZE: u64 = 512; +const DEFAULT_BLOCK_SIZE: u64 = 1024; +const DEFAULT_FILE_SIZE_BLOCK_SIZE: u64 = 1; + +pub(crate) enum Dereference { + None, + DirArgs, + Args, + All, +} + +#[derive(PartialEq, Eq)] +pub(crate) enum Sort { + None, + Name, + Size, + Time, + Version, + Extension, + Width, +} + +#[derive(PartialEq, Eq)] +pub(crate) enum Files { + All, + AlmostAll, + Normal, +} + +pub struct Config { + // Dir and vdir needs access to this field + pub format: Format, + pub(crate) files: Files, + pub(crate) sort: Sort, + pub(crate) recursive: bool, + pub(crate) reverse: bool, + pub(crate) dereference: Dereference, + pub(crate) ignore_patterns: Vec, + pub(crate) size_format: SizeFormat, + pub(crate) directory: bool, + pub(crate) time: MetadataTimeField, + #[cfg(unix)] + pub(crate) inode: bool, + pub(crate) color: Option, + pub(crate) long: LongFormat, + pub(crate) alloc_size: bool, + pub(crate) file_size_block_size: u64, + #[allow(dead_code)] + pub(crate) block_size: u64, // is never read on Windows + pub(crate) width: u16, + // Dir and vdir needs access to this field + pub quoting_style: QuotingStyle, + pub(crate) locale_quoting: Option, + pub(crate) indicator_style: IndicatorStyle, + pub(crate) time_format_recent: String, // Time format for recent dates + pub(crate) time_format_older: Option, // Time format for older dates (optional, if not present, time_format_recent is used) + pub(crate) context: bool, + #[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))] + pub(crate) selinux_supported: bool, + #[cfg(all(feature = "smack", target_os = "linux"))] + pub(crate) smack_supported: bool, + pub(crate) group_directories_first: bool, + pub(crate) line_ending: LineEnding, + pub(crate) dired: bool, + pub(crate) hyperlink: bool, + pub(crate) tab_size: usize, +} + +/// Extracts the format to display the information based on the options provided. +/// +/// # Returns +/// +/// A tuple containing the Format variant and an Option containing a &'static str +/// which corresponds to the option used to define the format. +fn extract_format(options: &clap::ArgMatches) -> (Format, Option<&'static str>) { + if let Some(format_) = options.get_one::(options::FORMAT) { + ( + match format_.as_str() { + "long" | "verbose" => Format::Long, + "single-column" => Format::OneLine, + "columns" | "vertical" => Format::Columns, + "across" | "horizontal" => Format::Across, + "commas" => Format::Commas, + // below should never happen as clap already restricts the values. + _ => unreachable!("Invalid field for --format"), + }, + Some(options::FORMAT), + ) + } else if options.get_flag(options::format::LONG) { + (Format::Long, Some(options::format::LONG)) + } else if options.get_flag(options::format::ACROSS) { + (Format::Across, Some(options::format::ACROSS)) + } else if options.get_flag(options::format::COMMAS) { + (Format::Commas, Some(options::format::COMMAS)) + } else if options.get_flag(options::format::COLUMNS) { + (Format::Columns, Some(options::format::COLUMNS)) + } else if stdout().is_terminal() { + (Format::Columns, None) + } else { + (Format::OneLine, None) + } +} + +/// Extracts the type of files to display +/// +/// # Returns +/// +/// A Files variant representing the type of files to display. +fn extract_files(options: &clap::ArgMatches) -> Files { + let get_last_index = |flag: &str| -> usize { + if options.value_source(flag) == Some(clap::parser::ValueSource::CommandLine) { + options.index_of(flag).unwrap_or(0) + } else { + 0 + } + }; + + let all_index = get_last_index(options::files::ALL); + let almost_all_index = get_last_index(options::files::ALMOST_ALL); + let unsorted_all_index = get_last_index(options::files::UNSORTED_ALL); + + let max_index = all_index.max(almost_all_index).max(unsorted_all_index); + + if max_index == 0 { + Files::Normal + } else if max_index == almost_all_index { + Files::AlmostAll + } else { + // Either -a or -f wins, both show all files + Files::All + } +} + +/// Extracts the sorting method to use based on the options provided. +/// +/// # Returns +/// +/// A Sort variant representing the sorting method to use. +fn extract_sort(options: &clap::ArgMatches) -> Sort { + let get_last_index = |flag: &str| -> usize { + if options.value_source(flag) == Some(clap::parser::ValueSource::CommandLine) { + options.index_of(flag).unwrap_or(0) + } else { + 0 + } + }; + + let sort_index = options + .get_one::(options::SORT) + .and_then(|_| options.indices_of(options::SORT)) + .map_or(0, |mut indices| indices.next_back().unwrap_or(0)); + let time_index = get_last_index(options::sort::TIME); + let size_index = get_last_index(options::sort::SIZE); + let none_index = get_last_index(options::sort::NONE); + let version_index = get_last_index(options::sort::VERSION); + let extension_index = get_last_index(options::sort::EXTENSION); + let unsorted_all_index = get_last_index(options::files::UNSORTED_ALL); + + let max_sort_index = sort_index + .max(time_index) + .max(size_index) + .max(none_index) + .max(version_index) + .max(extension_index) + .max(unsorted_all_index); + + match max_sort_index { + 0 => { + // No sort flags specified, use default behavior + if !options.get_flag(options::format::LONG) + && (options.get_flag(options::time::ACCESS) + || options.get_flag(options::time::CHANGE) + || options.get_one::(options::TIME).is_some()) + { + Sort::Time + } else { + Sort::Name + } + } + idx if idx == unsorted_all_index || idx == none_index => Sort::None, + idx if idx == sort_index => { + if let Some(field) = options.get_one::(options::SORT) { + match field.as_str() { + "none" => Sort::None, + "name" => Sort::Name, + "time" => Sort::Time, + "size" => Sort::Size, + "version" => Sort::Version, + "extension" => Sort::Extension, + "width" => Sort::Width, + _ => unreachable!("Invalid field for --sort"), + } + } else { + Sort::Name + } + } + idx if idx == time_index => Sort::Time, + idx if idx == size_index => Sort::Size, + idx if idx == version_index => Sort::Version, + idx if idx == extension_index => Sort::Extension, + _ => Sort::Name, + } +} + +/// Extracts the time to use based on the options provided. +/// +/// # Returns +/// +/// A `MetadataTimeField` variant representing the time to use. +fn extract_time(options: &clap::ArgMatches) -> MetadataTimeField { + if let Some(field) = options.get_one::(options::TIME) { + field.as_str().into() + } else if options.get_flag(options::time::ACCESS) { + MetadataTimeField::Access + } else if options.get_flag(options::time::CHANGE) { + MetadataTimeField::Change + } else { + MetadataTimeField::Modification + } +} + +/// Some env variables can be passed +/// For now, we are only verifying if empty or not and known for `TERM` +fn is_color_compatible_term() -> bool { + let is_term_set = std::env::var("TERM").is_ok(); + let is_colorterm_set = std::env::var("COLORTERM").is_ok(); + + let term = std::env::var("TERM").unwrap_or_default(); + let colorterm = std::env::var("COLORTERM").unwrap_or_default(); + + // Search function in the TERM struct to manage the wildcards + let term_matches = |term: &str| -> bool { + uucore::colors::TERMS.iter().any(|&pattern| { + term == pattern + || (pattern.ends_with('*') && term.starts_with(&pattern[..pattern.len() - 1])) + }) + }; + + if is_term_set && term.is_empty() && is_colorterm_set && colorterm.is_empty() { + return false; + } + + if !term.is_empty() && !term_matches(&term) { + return false; + } + true +} + +/// Extracts the color option to use based on the options provided. +/// +/// # Returns +/// +/// A boolean representing whether or not to use color. +fn extract_color(options: &clap::ArgMatches) -> bool { + if !is_color_compatible_term() { + return false; + } + + let get_last_index = |flag: &str| -> usize { + if options.value_source(flag) == Some(clap::parser::ValueSource::CommandLine) { + options.index_of(flag).unwrap_or(0) + } else { + 0 + } + }; + + let color_index = options + .get_one::(options::COLOR) + .and_then(|_| options.indices_of(options::COLOR)) + .map_or(0, |mut indices| indices.next_back().unwrap_or(0)); + let unsorted_all_index = get_last_index(options::files::UNSORTED_ALL); + + let color_enabled = match options.get_one::(options::COLOR) { + None => options.contains_id(options::COLOR), + Some(val) => match val.as_str() { + "" | "always" | "yes" | "force" => true, + "auto" | "tty" | "if-tty" => stdout().is_terminal(), + /* "never" | "no" | "none" | */ _ => false, + }, + }; + + // If --color was explicitly specified, always honor it regardless of -f + // Otherwise, if -f is present without explicit color, disable color + if color_index > 0 { + // Color was explicitly specified + color_enabled + } else if unsorted_all_index > 0 { + // -f present without explicit color, disable implicit color + false + } else { + color_enabled + } +} + +/// Extracts the hyperlink option to use based on the options provided. +/// +/// # Returns +/// +/// A boolean representing whether to hyperlink files. +fn extract_hyperlink(options: &clap::ArgMatches) -> bool { + let hyperlink = options + .get_one::(options::HYPERLINK) + .unwrap() + .as_str(); + + match hyperlink { + "always" | "yes" | "force" => true, + "auto" | "tty" | "if-tty" => stdout().is_terminal(), + "never" | "no" | "none" => false, + _ => unreachable!("should be handled by clap"), + } +} + +/// Match the argument given to --quoting-style or the [`QUOTING_STYLE`] env variable. +/// +/// # Arguments +/// +/// * `style`: the actual argument string +/// * `show_control` - A boolean value representing whether to show control characters. +/// +/// # Returns +/// +/// * An option with None if the style string is invalid, or a `QuotingStyle` wrapped in `Some`. +struct QuotingStyleSpec { + style: QuotingStyle, + fixed_control: bool, + locale: Option, +} + +impl QuotingStyleSpec { + fn new(style: QuotingStyle) -> Self { + Self { + style, + fixed_control: false, + locale: None, + } + } + + fn with_locale(style: QuotingStyle, locale: LocaleQuoting) -> Self { + Self { + style, + fixed_control: true, + locale: Some(locale), + } + } +} +fn match_quoting_style_name( + style: &str, + show_control: bool, +) -> Option<(QuotingStyle, Option)> { + let spec = match style { + "literal" => QuotingStyleSpec::new(QuotingStyle::Literal { + show_control: false, + }), + "shell" => QuotingStyleSpec::new(QuotingStyle::SHELL), + "shell-always" => QuotingStyleSpec::new(QuotingStyle::SHELL_QUOTE), + "shell-escape" => QuotingStyleSpec::new(QuotingStyle::SHELL_ESCAPE), + "shell-escape-always" => QuotingStyleSpec::new(QuotingStyle::SHELL_ESCAPE_QUOTE), + "c" => QuotingStyleSpec::new(QuotingStyle::C_DOUBLE), + "escape" => QuotingStyleSpec::new(QuotingStyle::C_NO_QUOTES), + "locale" => QuotingStyleSpec { + style: QuotingStyle::Literal { + show_control: false, + }, + fixed_control: true, + locale: Some(LocaleQuoting::Single), + }, + "clocale" => QuotingStyleSpec::with_locale(QuotingStyle::C_DOUBLE, LocaleQuoting::Double), + _ => return None, + }; + + let style = if spec.fixed_control { + spec.style + } else { + spec.style.show_control(show_control) + }; + + Some((style, spec.locale)) +} + +/// Extracts the quoting style to use based on the options provided. +/// If no options are given, it looks if a default quoting style is provided +/// through the [`QUOTING_STYLE`] environment variable. +/// +/// # Arguments +/// +/// * `options` - A reference to a [`clap::ArgMatches`] object containing command line arguments. +/// * `show_control` - A boolean value representing whether or not to show control characters. +/// +/// # Returns +/// +/// A [`QuotingStyle`] variant representing the quoting style to use. +fn extract_quoting_style( + options: &clap::ArgMatches, + show_control: bool, +) -> (QuotingStyle, Option) { + let opt_quoting_style = options.get_one::(QUOTING_STYLE); + + if let Some(style) = opt_quoting_style { + match match_quoting_style_name(style, show_control) { + Some(pair) => pair, + None => unreachable!("Should have been caught by Clap"), + } + } else if options.get_flag(options::quoting::LITERAL) { + (QuotingStyle::Literal { show_control }, None) + } else if options.get_flag(options::quoting::ESCAPE) { + (QuotingStyle::C_NO_QUOTES, None) + } else if options.get_flag(options::quoting::C) { + (QuotingStyle::C_DOUBLE, None) + } else if options.get_flag(options::DIRED) { + (QuotingStyle::Literal { show_control }, None) + } else { + // If set, the QUOTING_STYLE environment variable specifies a default style. + if let Ok(style) = std::env::var("QUOTING_STYLE") { + match match_quoting_style_name(style.as_str(), show_control) { + Some(pair) => return pair, + None => eprintln!( + "{}", + translate!("ls-invalid-quoting-style", "program" => std::env::args().next().unwrap_or_else(|| "ls".to_string()), "style" => style.clone()) + ), + } + } + + // By default, `ls` uses Shell escape quoting style when writing to a terminal file + // descriptor and Literal otherwise. + if stdout().is_terminal() { + (QuotingStyle::SHELL_ESCAPE.show_control(show_control), None) + } else { + (QuotingStyle::Literal { show_control }, None) + } + } +} + +/// Extracts the indicator style to use based on the options provided. +/// +/// # Returns +/// +/// An [`IndicatorStyle`] variant representing the indicator style to use. +fn extract_indicator_style(options: &clap::ArgMatches) -> IndicatorStyle { + if let Some(field) = options.get_one::(options::INDICATOR_STYLE) { + match field.as_str() { + "none" => IndicatorStyle::None, + "file-type" => IndicatorStyle::FileType, + "classify" => IndicatorStyle::Classify, + "slash" => IndicatorStyle::Slash, + &_ => IndicatorStyle::None, + } + } else if let Some(field) = options.get_one::(options::indicator_style::CLASSIFY) { + match field.as_str() { + "never" | "no" | "none" => IndicatorStyle::None, + "always" | "yes" | "force" => IndicatorStyle::Classify, + "auto" | "tty" | "if-tty" => { + if stdout().is_terminal() { + IndicatorStyle::Classify + } else { + IndicatorStyle::None + } + } + &_ => IndicatorStyle::None, + } + } else if options.get_flag(options::indicator_style::SLASH) { + IndicatorStyle::Slash + } else if options.get_flag(options::indicator_style::FILE_TYPE) { + IndicatorStyle::FileType + } else { + IndicatorStyle::None + } +} + +/// Parses the width value from either the command line arguments or the environment variables. +fn parse_width(width_match: Option<&String>) -> Result { + let parse_width_from_args = |s: &str| -> Result { + let radix = if s.starts_with('0') && s.len() > 1 { + 8 + } else { + 10 + }; + match u16::from_str_radix(s, radix) { + Ok(x) => Ok(x), + Err(e) => match e.kind() { + IntErrorKind::PosOverflow => Ok(u16::MAX), + _ => Err(LsError::InvalidLineWidth(s.into())), + }, + } + }; + + let parse_width_from_env = |columns: OsString| { + if let Some(columns) = columns.to_str().and_then(|s| s.parse().ok()) { + columns + } else { + show_error!( + "{}", + translate!("ls-invalid-columns-width", "width" => columns.quote()) + ); + DEFAULT_TERM_WIDTH + } + }; + + let calculate_term_size = || match terminal_size::terminal_size() { + Some((width, _)) => width.0, + None => DEFAULT_TERM_WIDTH, + }; + + let ret = match width_match { + Some(x) => parse_width_from_args(x)?, + None => match std::env::var_os("COLUMNS") { + Some(columns) => parse_width_from_env(columns), + None => calculate_term_size(), + }, + }; + + Ok(ret) +} + +impl Config { + #[allow(clippy::cognitive_complexity)] + pub fn from(options: &clap::ArgMatches) -> UResult { + let context = options.get_flag(options::CONTEXT); + let (mut format, opt) = extract_format(options); + let files = extract_files(options); + + // The -o, -n and -g options are tricky. They cannot override with each + // other because it's possible to combine them. For example, the option + // -og should hide both owner and group. Furthermore, they are not + // reset if -l or --format=long is used. So these should just show the + // group: -gl or "-g --format=long". Finally, they are also not reset + // when switching to a different format option in-between like this: + // -ogCl or "-og --format=vertical --format=long". + // + // -1 has a similar issue: it does nothing if the format is long. This + // actually makes it distinct from the --format=singe-column option, + // which always applies. + // + // The idea here is to not let these options override with the other + // options, but manually whether they have an index that's greater than + // the other format options. If so, we set the appropriate format. + if format != Format::Long { + let idx = opt + .and_then(|opt| options.indices_of(opt).map(|x| x.max().unwrap())) + .unwrap_or(0); + if [ + options::format::LONG_NO_OWNER, + options::format::LONG_NO_GROUP, + options::format::LONG_NUMERIC_UID_GID, + options::FULL_TIME, + ] + .iter() + .filter_map(|opt| { + if options.value_source(opt) == Some(clap::parser::ValueSource::CommandLine) { + options.indices_of(opt) + } else { + None + } + }) + .flatten() + .any(|i| i >= idx) + { + format = Format::Long; + } else if let Some(mut indices) = options.indices_of(options::format::ONE_LINE) { + if options.value_source(options::format::ONE_LINE) + == Some(clap::parser::ValueSource::CommandLine) + && indices.any(|i| i > idx) + { + format = Format::OneLine; + } + } + } + + let sort = extract_sort(options); + let time = extract_time(options); + let mut needs_color = extract_color(options); + let hyperlink = extract_hyperlink(options); + + let opt_block_size = options.get_one::(options::size::BLOCK_SIZE); + let opt_si = opt_block_size.is_some() + && options + .get_one::(options::size::BLOCK_SIZE) + .unwrap() + .eq("si") + || options.get_flag(options::size::SI); + let opt_hr = (opt_block_size.is_some() + && options + .get_one::(options::size::BLOCK_SIZE) + .unwrap() + .eq("human-readable")) + || options.get_flag(options::size::HUMAN_READABLE); + let opt_kb = options.get_flag(options::size::KIBIBYTES); + + let size_format = if opt_si { + SizeFormat::Decimal + } else if opt_hr { + SizeFormat::Binary + } else { + SizeFormat::Bytes + }; + + let env_var_blocksize = std::env::var_os("BLOCKSIZE"); + let env_var_block_size = std::env::var_os("BLOCK_SIZE"); + let env_var_ls_block_size = std::env::var_os("LS_BLOCK_SIZE"); + let env_var_posixly_correct = std::env::var_os("POSIXLY_CORRECT"); + let mut is_env_var_blocksize = false; + + let raw_block_size = if let Some(opt_block_size) = opt_block_size { + OsString::from(opt_block_size) + } else if let Some(env_var_ls_block_size) = env_var_ls_block_size { + env_var_ls_block_size + } else if let Some(env_var_block_size) = env_var_block_size { + env_var_block_size + } else if let Some(env_var_blocksize) = env_var_blocksize { + is_env_var_blocksize = true; + env_var_blocksize + } else { + OsString::from("") + }; + + let (file_size_block_size, block_size) = if !opt_si && !opt_hr && !raw_block_size.is_empty() + { + if let Ok(size) = parse_size_non_zero_u64(&raw_block_size.to_string_lossy()) { + 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), + (false, true) => { + // --block-size overrides -k + if opt_block_size.is_some() { + (size, size) + } else { + (size, DEFAULT_BLOCK_SIZE) + } + } + (false, false) => (size, size), + } + } else { + // only fail if invalid block size was specified with --block-size, + // ignore invalid block size from env vars + if let Some(invalid_block_size) = opt_block_size { + return Err(Box::new(LsError::BlockSizeParseError( + invalid_block_size.clone(), + ))); + } + if is_env_var_blocksize { + (DEFAULT_FILE_SIZE_BLOCK_SIZE, DEFAULT_BLOCK_SIZE) + } else { + (DEFAULT_BLOCK_SIZE, DEFAULT_BLOCK_SIZE) + } + } + } else if env_var_posixly_correct.is_some() { + if opt_kb { + (DEFAULT_FILE_SIZE_BLOCK_SIZE, DEFAULT_BLOCK_SIZE) + } else { + (DEFAULT_FILE_SIZE_BLOCK_SIZE, POSIXLY_CORRECT_BLOCK_SIZE) + } + } else if opt_si { + (DEFAULT_FILE_SIZE_BLOCK_SIZE, 1000) + } else { + (DEFAULT_FILE_SIZE_BLOCK_SIZE, DEFAULT_BLOCK_SIZE) + }; + + let long = { + let author = options.get_flag(options::AUTHOR); + let group = !options.get_flag(options::NO_GROUP) + && !options.get_flag(options::format::LONG_NO_GROUP); + let owner = !options.get_flag(options::format::LONG_NO_OWNER); + #[cfg(unix)] + let numeric_uid_gid = options.get_flag(options::format::LONG_NUMERIC_UID_GID); + LongFormat { + author, + group, + owner, + #[cfg(unix)] + numeric_uid_gid, + } + }; + let width = parse_width(options.get_one::(options::WIDTH))?; + + #[allow(clippy::needless_bool)] + let mut show_control = if options.get_flag(options::HIDE_CONTROL_CHARS) { + false + } else if options.get_flag(options::SHOW_CONTROL_CHARS) { + true + } else { + !stdout().is_terminal() + }; + + let (mut quoting_style, mut locale_quoting) = extract_quoting_style(options, show_control); + let indicator_style = extract_indicator_style(options); + // Only parse the value to "--time-style" if it will become relevant. + let dired = options.get_flag(options::DIRED); + let (time_format_recent, time_format_older) = if format == Format::Long || dired { + parse_time_style(options)? + } else { + Default::default() + }; + + let mut ignore_patterns: Vec = Vec::new(); + + if options.get_flag(options::IGNORE_BACKUPS) { + ignore_patterns.push(Pattern::new("*~").unwrap()); + ignore_patterns.push(Pattern::new(".*~").unwrap()); + } + + for pattern in options + .get_many::(options::IGNORE) + .into_iter() + .flatten() + { + if let Ok(p) = parse_glob::from_str(pattern) { + ignore_patterns.push(p); + } else { + show_warning!( + "{}", + translate!("ls-invalid-ignore-pattern", "pattern" => pattern.quote()) + ); + } + } + + if files == Files::Normal { + for pattern in options + .get_many::(options::HIDE) + .into_iter() + .flatten() + { + if let Ok(p) = parse_glob::from_str(pattern) { + ignore_patterns.push(p); + } else { + show_warning!( + "{}", + translate!("ls-invalid-hide-pattern", "pattern" => pattern.quote()) + ); + } + } + } + + // According to ls info page, `--zero` implies the following flags: + // - `--show-control-chars` + // - `--format=single-column` + // - `--color=none` + // - `--quoting-style=literal` + // Current GNU ls implementation allows `--zero` Behavior to be + // overridden by later flags. + let zero_formats_opts = [ + options::format::ACROSS, + options::format::COLUMNS, + options::format::COMMAS, + options::format::LONG, + options::format::LONG_NO_GROUP, + options::format::LONG_NO_OWNER, + options::format::LONG_NUMERIC_UID_GID, + options::format::ONE_LINE, + options::FORMAT, + ]; + let zero_colors_opts = [options::COLOR]; + let zero_show_control_opts = [options::HIDE_CONTROL_CHARS, options::SHOW_CONTROL_CHARS]; + let zero_quoting_style_opts = [ + QUOTING_STYLE, + options::quoting::C, + options::quoting::ESCAPE, + options::quoting::LITERAL, + ]; + let get_last = |flag: &str| -> usize { + if options.value_source(flag) == Some(clap::parser::ValueSource::CommandLine) { + options.index_of(flag).unwrap_or(0) + } else { + 0 + } + }; + if get_last(options::ZERO) + > zero_formats_opts + .into_iter() + .map(get_last) + .max() + .unwrap_or(0) + { + format = if format == Format::Long { + format + } else { + Format::OneLine + }; + } + if get_last(options::ZERO) + > zero_colors_opts + .into_iter() + .map(get_last) + .max() + .unwrap_or(0) + { + needs_color = false; + } + if get_last(options::ZERO) + > zero_show_control_opts + .into_iter() + .map(get_last) + .max() + .unwrap_or(0) + { + show_control = true; + } + if get_last(options::ZERO) + > zero_quoting_style_opts + .into_iter() + .map(get_last) + .max() + .unwrap_or(0) + { + quoting_style = QuotingStyle::Literal { show_control }; + locale_quoting = None; + } + + if needs_color { + if let Err(err) = validate_ls_colors_env() { + if let LsColorsParseError::UnrecognizedPrefix(prefix) = &err { + show_warning!( + "{}", + translate!( + "ls-warning-unrecognized-ls-colors-prefix", + "prefix" => prefix.quote() + ) + ); + } + show_warning!("{}", translate!("ls-warning-unparsable-ls-colors")); + needs_color = false; + } + } + + let color = if needs_color { + Some(LsColors::from_env().unwrap_or_default()) + } else { + None + }; + + if dired || is_dired_arg_present() { + // --dired implies --format=long + // if we have --dired --hyperlink, we don't show dired but we still want to see the + // long format + format = Format::Long; + } + if dired && options.get_flag(options::ZERO) { + return Err(Box::new(LsError::DiredAndZeroAreIncompatible)); + } + + let dereference = if options.get_flag(options::dereference::ALL) { + Dereference::All + } else if options.get_flag(options::dereference::ARGS) { + Dereference::Args + } else if options.get_flag(options::dereference::DIR_ARGS) { + Dereference::DirArgs + } else if options.get_flag(options::DIRECTORY) + || indicator_style == IndicatorStyle::Classify + || format == Format::Long + { + Dereference::None + } else { + Dereference::DirArgs + }; + + let tab_size = if needs_color { + Some(0) + } else { + options + .get_one::(options::format::TAB_SIZE) + .and_then(|size| size.parse::().ok()) + .or_else(|| std::env::var("TABSIZE").ok().and_then(|s| s.parse().ok())) + } + .unwrap_or(SPACES_IN_TAB); + + Ok(Self { + format, + files, + sort, + recursive: options.get_flag(options::RECURSIVE), + reverse: options.get_flag(options::REVERSE), + dereference, + ignore_patterns, + size_format, + directory: options.get_flag(options::DIRECTORY), + time, + color, + #[cfg(unix)] + inode: options.get_flag(options::INODE), + long, + alloc_size: options.get_flag(options::size::ALLOCATION_SIZE), + file_size_block_size, + block_size, + width, + quoting_style, + locale_quoting, + indicator_style, + time_format_recent, + time_format_older, + context, + #[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))] + selinux_supported: uucore::selinux::is_selinux_enabled(), + #[cfg(all(feature = "smack", target_os = "linux"))] + smack_supported: uucore::smack::is_smack_enabled(), + group_directories_first: options.get_flag(options::GROUP_DIRECTORIES_FIRST), + line_ending: LineEnding::from_zero_flag(options.get_flag(options::ZERO)), + dired, + hyperlink, + tab_size, + }) + } +} + +fn parse_time_style(options: &clap::ArgMatches) -> Result<(String, Option), LsError> { + // TODO: Using correct locale string is not implemented. + const LOCALE_FORMAT: (&str, Option<&str>) = ("%b %e %H:%M", Some("%b %e %Y")); + + // Convert time_styles references to owned String/option. + #[expect(clippy::unnecessary_wraps, reason = "internal result helper")] + fn ok((recent, older): (&str, Option<&str>)) -> Result<(String, Option), LsError> { + Ok((recent.to_string(), older.map(String::from))) + } + + if let Some(field) = options + .get_one::(options::TIME_STYLE) + .map(ToOwned::to_owned) + .or_else(|| std::env::var("TIME_STYLE").ok()) + { + //If both FULL_TIME and TIME_STYLE are present + //The one added last is dominant + if options.get_flag(options::FULL_TIME) + && options.indices_of(options::FULL_TIME).unwrap().next_back() + > options.indices_of(options::TIME_STYLE).unwrap().next_back() + { + ok((format::FULL_ISO, None)) + } else { + let field = if let Some(field) = field.strip_prefix("posix-") { + // See GNU documentation, set format to "locale" if LC_TIME="POSIX", + // else just strip the prefix and continue (even "posix+FORMAT" is + // supported). + // TODO: This needs to be moved to uucore and handled by icu? + if std::env::var_os("LC_TIME").as_deref() == Some(OsStr::new("POSIX")) + || std::env::var_os("LC_ALL").as_deref() == Some(OsStr::new("POSIX")) + { + return ok(LOCALE_FORMAT); + } + field + } else { + &field + }; + + match field { + "full-iso" => ok((format::FULL_ISO, None)), + "long-iso" => ok((format::LONG_ISO, None)), + // ISO older format needs extra padding. + "iso" => Ok(( + "%m-%d %H:%M".to_string(), + Some(format::ISO.to_string() + " "), + )), + "locale" => ok(LOCALE_FORMAT), + _ => match field.chars().next().unwrap() { + '+' => { + // recent/older formats are (optionally) separated by a newline + let mut it = field[1..].split('\n'); + let recent = it.next().unwrap_or_default(); + let older = it.next(); + match it.next() { + None => ok((recent, older)), + Some(_) => Err(LsError::TimeStyleParseError(String::from(field))), + } + } + _ => Err(LsError::TimeStyleParseError(String::from(field))), + }, + } + } + } else if options.get_flag(options::FULL_TIME) { + ok((format::FULL_ISO, None)) + } else { + ok(LOCALE_FORMAT) + } +} diff --git a/src/uu/ls/src/display.rs b/src/uu/ls/src/display.rs new file mode 100644 index 00000000000..e080b1ad1fd --- /dev/null +++ b/src/uu/ls/src/display.rs @@ -0,0 +1,1314 @@ +// spell-checker:ignore (ToDO) somegroup nlink tabsize dired subdired dtype colorterm stringly +// spell-checker:ignore nohash strtime clocale + +use std::cell::LazyCell; +#[cfg(unix)] +use std::os::unix::fs::{FileTypeExt, MetadataExt}; +#[cfg(windows)] +use std::os::windows::fs::MetadataExt; +/// Show the directory name in the case where several arguments are given to ls +use std::{borrow::Cow, iter}; +use std::{ + ffi::{OsStr, OsString}, + fmt::Write as _, + fs::{self, DirEntry, FileType, Metadata}, + io::{BufWriter, Stdout, Write}, +}; + +use ansi_width::ansi_width; +use glob::MatchOptions; +use term_grid::{DEFAULT_SEPARATOR_SIZE, Direction, Filling, Grid, GridOptions}; + +#[cfg(unix)] +use uucore::entries; +#[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] +use uucore::fsxattr::has_acl; +#[cfg(any( + target_os = "linux", + target_os = "macos", + target_os = "android", + target_os = "ios", + target_os = "freebsd", + target_os = "dragonfly", + target_os = "netbsd", + target_os = "openbsd", + target_os = "illumos", + target_os = "solaris" +))] +use uucore::libc::{dev_t, major, minor}; +use uucore::{ + error::UResult, + format::human::human_readable, + fs::display_permissions, + fsext::metadata_get_time, + line_ending::LineEnding, + os_str_as_bytes_lossy, + quoting_style::{QuotingStyle, locale_aware_escape_dir_name, locale_aware_escape_name}, + show, + time::{FormatSystemTimeFallback, format_system_time}, +}; + +use crate::colors::color_name; +use crate::config::Files; +use crate::dired::{self, DiredOutput}; +use crate::{Config, ListState, LsError, PathData, get_block_size}; + +// Fields that can be removed or added to the long format +pub(crate) struct LongFormat { + pub(crate) author: bool, + pub(crate) group: bool, + pub(crate) owner: bool, + #[cfg(unix)] + pub(crate) numeric_uid_gid: bool, +} + +pub(crate) struct PaddingCollection { + #[cfg(unix)] + pub(crate) inode: usize, + pub(crate) link_count: usize, + pub(crate) uname: usize, + pub(crate) group: usize, + pub(crate) context: usize, + pub(crate) size: usize, + #[cfg(unix)] + pub(crate) major: usize, + #[cfg(unix)] + pub(crate) minor: usize, + pub(crate) block_size: usize, +} + +pub(crate) struct DisplayItemName { + pub(crate) displayed: OsString, + pub(crate) dired_name_len: usize, +} + +#[derive(PartialEq, Eq)] +pub(crate) enum IndicatorStyle { + None, + Slash, + FileType, + Classify, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +pub(crate) enum LocaleQuoting { + Single, + Double, +} + +#[derive(PartialEq, Eq, Debug)] +pub enum Format { + Columns, + Long, + OneLine, + Across, + Commas, +} + +#[allow(dead_code)] +enum SizeOrDeviceId { + Size(String), + Device(String, String), +} + +/// or the recursive flag is passed. +/// +/// ```no-exec +/// $ ls -R +/// .: <- This is printed by this function +/// dir1 file1 file2 +/// +/// dir1: <- This as well +/// file11 +/// ``` +pub fn show_dir_name( + path_data: &PathData, + out: &mut BufWriter, + config: &Config, +) -> std::io::Result<()> { + let escaped_name = escape_dir_name_with_locale(path_data.path().as_os_str(), config); + + let name = if config.hyperlink && !config.dired { + create_hyperlink(&escaped_name, path_data) + } else { + escaped_name + }; + + write_os_str(out, &name)?; + write!(out, ":") +} + +fn escape_with_locale(name: &OsStr, config: &Config, fallback: F) -> OsString +where + F: FnOnce(&OsStr, QuotingStyle) -> OsString, +{ + if let Some(locale) = config.locale_quoting { + locale_quote(name, locale) + } else { + fallback(name, config.quoting_style) + } +} + +fn escape_dir_name_with_locale(name: &OsStr, config: &Config) -> OsString { + escape_with_locale(name, config, locale_aware_escape_dir_name) +} + +fn escape_name_with_locale(name: &OsStr, config: &Config) -> OsString { + escape_with_locale(name, config, locale_aware_escape_name) +} + +fn locale_quote(name: &OsStr, style: LocaleQuoting) -> OsString { + let bytes = os_str_as_bytes_lossy(name); + let mut quoted = String::new(); + match style { + LocaleQuoting::Single => quoted.push('\''), + LocaleQuoting::Double => quoted.push('"'), + } + for &byte in bytes.as_ref() { + push_locale_byte(&mut quoted, byte, style); + } + match style { + LocaleQuoting::Single => quoted.push('\''), + LocaleQuoting::Double => quoted.push('"'), + } + OsString::from(quoted) +} + +fn push_locale_byte(buf: &mut String, byte: u8, style: LocaleQuoting) { + match (style, byte) { + (LocaleQuoting::Single, b'\'') => buf.push_str("'\\''"), + (LocaleQuoting::Double, b'"') => buf.push_str("\\\""), + (_, b'\\') => buf.push_str("\\\\"), + _ => push_basic_escape(buf, byte), + } +} + +fn push_basic_escape(buf: &mut String, byte: u8) { + match byte { + b'\x07' => buf.push_str("\\a"), + b'\x08' => buf.push_str("\\b"), + b'\t' => buf.push_str("\\t"), + b'\n' => buf.push_str("\\n"), + b'\x0b' => buf.push_str("\\v"), + b'\x0c' => buf.push_str("\\f"), + b'\r' => buf.push_str("\\r"), + b'\x1b' => buf.push_str("\\e"), + b'"' => buf.push('"'), + b'\'' => buf.push('\''), + b if (0x20..=0x7e).contains(&b) => buf.push(b as char), + _ => { + let _ = write!(buf, "\\{byte:03o}"); + } + } +} + +pub fn should_display(entry: &DirEntry, config: &Config) -> bool { + // check if hidden + if config.files == Files::Normal && is_hidden(entry) { + return false; + } + + // check if it is among ignore_patterns + let options = MatchOptions { + // setting require_literal_leading_dot to match behavior in GNU ls + require_literal_leading_dot: true, + require_literal_separator: false, + case_sensitive: true, + }; + + let file_name = entry.file_name(); + // If the decoding fails, still match best we can + // FIXME: use OsStrings or Paths once we have a glob crate that supports it: + // https://github.com/rust-lang/glob/issues/23 + // https://github.com/rust-lang/glob/issues/78 + // https://github.com/BurntSushi/ripgrep/issues/1250 + + let file_name = match file_name.to_str() { + Some(s) => Cow::Borrowed(s), + None => file_name.to_string_lossy(), + }; + + !config + .ignore_patterns + .iter() + .any(|p| p.matches_with(&file_name, options)) +} + +fn display_dir_entry_size( + entry: &PathData, + config: &Config, + state: &mut ListState, +) -> (usize, usize, usize, usize, usize, usize) { + // TODO: Cache/memorize the display_* results so we don't have to recalculate them. + if let Some(md) = entry.metadata() { + let (size_len, major_len, minor_len) = match display_len_or_rdev(md, config) { + SizeOrDeviceId::Device(major, minor) => { + (major.len() + minor.len() + 2usize, major.len(), minor.len()) + } + SizeOrDeviceId::Size(size) => (size.len(), 0usize, 0usize), + }; + ( + display_symlink_count(md).len(), + display_uname(md, config, state).len(), + display_group(md, config, state).len(), + size_len, + major_len, + minor_len, + ) + } else { + (0, 0, 0, 0, 0, 0) + } +} + +// A simple, performant, ExtendPad trait to add a string to a Vec, padding with spaces +// on the left or right, without making additional copies, or using formatting functions. +pub trait ExtendPad { + fn extend_pad_left(&mut self, string: &str, count: usize); + fn extend_pad_right(&mut self, string: &str, count: usize); +} + +impl ExtendPad for Vec { + fn extend_pad_left(&mut self, string: &str, count: usize) { + if string.len() < count { + self.extend(iter::repeat_n(b' ', count - string.len())); + } + self.extend(string.as_bytes()); + } + + fn extend_pad_right(&mut self, string: &str, count: usize) { + self.extend(string.as_bytes()); + if string.len() < count { + self.extend(iter::repeat_n(b' ', count - string.len())); + } + } +} + +// TODO: Consider converting callers to use ExtendPad instead, as it avoids +// additional copies. +fn pad_left(string: &str, count: usize) -> String { + format!("{string:>count$}") +} + +#[allow(clippy::cognitive_complexity)] +pub fn display_items( + items: &[PathData], + config: &Config, + state: &mut ListState, + dired: &mut DiredOutput, +) -> UResult<()> { + // `-Z`, `--context`: + // Display the SELinux security context or '?' if none is found. When used with the `-l` + // option, print the security context to the left of the size column. + + let quoted = items.iter().any(|item| { + let name = escape_name_with_locale(item.display_name(), config); + os_str_starts_with(&name, b"'") + }); + + if config.format == Format::Long { + let padding_collection = calculate_padding_collection(items, config, state); + + for item in items { + #[cfg(unix)] + let should_display_leading_info = config.inode || config.alloc_size; + #[cfg(not(unix))] + let should_display_leading_info = config.alloc_size; + + if should_display_leading_info { + let more_info = display_additional_leading_info(item, &padding_collection, config); + + write!(state.out, "{more_info}")?; + } + + display_item_long(item, &padding_collection, config, state, dired, quoted)?; + } + } else { + let mut longest_context_len = 1; + let prefix_context = if config.context { + for item in items { + let context_len = item.security_context(config).len(); + longest_context_len = context_len.max(longest_context_len); + } + Some(longest_context_len) + } else { + None + }; + + let padding = calculate_padding_collection(items, config, state); + + // we need to apply normal color to non filename output + if let Some(style_manager) = &mut state.style_manager { + write!(state.out, "{}", style_manager.apply_normal())?; + } + + let mut names_vec = Vec::new(); + + #[cfg(unix)] + let should_display_leading_info = config.inode || config.alloc_size; + #[cfg(not(unix))] + let should_display_leading_info = config.alloc_size; + + for i in items { + let more_info = if should_display_leading_info { + Some(display_additional_leading_info(i, &padding, config)) + } else { + None + }; + // it's okay to set current column to zero which is used to decide + // whether text will wrap or not, because when format is grid or + // column ls will try to place the item name in a new line if it + // wraps. + let cell = display_item_name( + i, + config, + prefix_context, + more_info, + state, + LazyCell::new(Box::new(|| 0)), + ); + + names_vec.push(cell.displayed); + } + + let mut names = names_vec.into_iter(); + + match config.format { + Format::Columns => { + display_grid( + names, + config.width, + Direction::TopToBottom, + &mut state.out, + quoted, + config.tab_size, + )?; + } + Format::Across => { + display_grid( + names, + config.width, + Direction::LeftToRight, + &mut state.out, + quoted, + config.tab_size, + )?; + } + Format::Commas => { + let mut current_col = 0; + if let Some(name) = names.next() { + write_os_str(&mut state.out, &name)?; + current_col = ansi_width(&name.to_string_lossy()) as u16 + 2; + } + for name in names { + let name_width = ansi_width(&name.to_string_lossy()) as u16; + // If the width is 0 we print one single line + if config.width != 0 && current_col + name_width + 1 > config.width { + current_col = name_width + 2; + writeln!(state.out, ",")?; + } else { + current_col += name_width + 2; + write!(state.out, ", ")?; + } + write_os_str(&mut state.out, &name)?; + } + // Current col is never zero again if names have been printed. + // So we print a newline. + if current_col > 0 { + write!(state.out, "{}", config.line_ending)?; + } + } + _ => { + for name in names { + write_os_str(&mut state.out, &name)?; + write!(state.out, "{}", config.line_ending)?; + } + } + } + } + + Ok(()) +} + +fn display_grid( + names: impl Iterator, + width: u16, + direction: Direction, + out: &mut BufWriter, + quoted: bool, + tab_size: usize, +) -> UResult<()> { + if width == 0 { + // If the width is 0 we print one single line + let mut printed_something = false; + for name in names { + if printed_something { + write!(out, " ")?; + } + printed_something = true; + write_os_str(out, &name)?; + } + if printed_something { + writeln!(out)?; + } + } else { + let names: Vec<_> = if quoted { + // In case some names are quoted, GNU adds a space before each + // entry that does not start with a quote to make it prettier + // on multiline. + // + // Example: + // ``` + // $ ls + // 'a\nb' bar + // foo baz + // ^ ^ + // These spaces is added + // ``` + names + .map(|n| { + if os_str_starts_with(&n, b"'") || os_str_starts_with(&n, b"\"") { + n + } else { + let mut ret: OsString = " ".into(); + ret.push(n); + ret + } + }) + .collect() + } else { + names.collect() + }; + + // FIXME: the Grid crate only supports &str, so can't display raw bytes + let names: Vec<_> = names + .into_iter() + .map(|s| s.to_string_lossy().into_owned()) + .collect(); + + // Since tab_size=0 means no \t, use Spaces separator for optimization. + let filling = match tab_size { + 0 => Filling::Spaces(DEFAULT_SEPARATOR_SIZE), + _ => Filling::Tabs { + spaces: DEFAULT_SEPARATOR_SIZE, + tab_size, + }, + }; + + let grid = Grid::new( + names, + GridOptions { + filling, + direction, + width: width as usize, + }, + ); + write!(out, "{grid}")?; + } + Ok(()) +} + +fn display_additional_leading_info( + item: &PathData, + padding: &PaddingCollection, + config: &Config, +) -> String { + let mut result = String::new(); + #[cfg(unix)] + { + if config.inode { + let i = if let Some(md) = item.metadata() { + get_inode(md) + } else { + "?".to_owned() + }; + write!(result, "{} ", pad_left(&i, padding.inode)).unwrap(); + } + } + + if config.alloc_size { + let s = if let Some(md) = item.metadata() { + display_size(get_block_size(md, config), config) + } else { + "?".to_owned() + }; + // extra space is insert to align the sizes, as needed for all formats, except for the comma format. + if config.format == Format::Commas { + write!(result, "{s} ").unwrap(); + } else { + write!(result, "{} ", pad_left(&s, padding.block_size)).unwrap(); + } + } + + result +} + +fn calculate_line_len(output_len: usize, item_len: usize, line_ending: LineEnding) -> usize { + output_len + item_len + line_ending.to_string().len() +} + +#[cfg(unix)] +fn get_inode(metadata: &Metadata) -> String { + format!("{}", metadata.ino()) +} + +// Currently getpwuid is `linux` target only. If it's broken state.out into +// a posix-compliant attribute this can be updated... +#[cfg(unix)] +fn display_uname<'a>(metadata: &Metadata, config: &Config, state: &'a mut ListState) -> &'a String { + let uid = metadata.uid(); + + state.uid_cache.entry(uid).or_insert_with(|| { + if config.long.numeric_uid_gid { + uid.to_string() + } else { + entries::uid2usr(uid).unwrap_or_else(|_| uid.to_string()) + } + }) +} + +#[cfg(unix)] +fn display_group<'a>(metadata: &Metadata, config: &Config, state: &'a mut ListState) -> &'a String { + let gid = metadata.gid(); + state.gid_cache.entry(gid).or_insert_with(|| { + if config.long.numeric_uid_gid { + gid.to_string() + } else { + entries::gid2grp(gid).unwrap_or_else(|_| gid.to_string()) + } + }) +} + +#[cfg(not(unix))] +fn display_uname(_metadata: &Metadata, _config: &Config, _state: &mut ListState) -> &'static str { + "somebody" +} + +#[cfg(not(unix))] +fn display_group(_metadata: &Metadata, _config: &Config, _state: &mut ListState) -> &'static str { + "somegroup" +} + +fn display_date( + metadata: &Metadata, + config: &Config, + state: &mut ListState, + out: &mut Vec, +) -> UResult<()> { + let Some(time) = metadata_get_time(metadata, config.time) else { + out.extend(b"???"); + return Ok(()); + }; + + // Use "recent" format if the given date is considered recent (i.e., in the last 6 months), + // or if no "older" format is available. + let fmt = match &config.time_format_older { + Some(time_format_older) if !state.recent_time_range.contains(&time) => time_format_older, + _ => &config.time_format_recent, + }; + + format_system_time(out, time, fmt, FormatSystemTimeFallback::Integer) +} + +fn display_len_or_rdev(metadata: &Metadata, config: &Config) -> SizeOrDeviceId { + #[cfg(any( + target_os = "linux", + target_os = "macos", + target_os = "android", + target_os = "ios", + target_os = "freebsd", + target_os = "dragonfly", + target_os = "netbsd", + target_os = "openbsd", + target_os = "illumos", + target_os = "solaris" + ))] + { + let ft = metadata.file_type(); + if ft.is_char_device() || ft.is_block_device() { + // A type cast is needed here as the `dev_t` type varies across OSes. + let dev = metadata.rdev() as dev_t; + let major = major(dev); + let minor = minor(dev); + return SizeOrDeviceId::Device(major.to_string(), minor.to_string()); + } + } + let len_adjusted = { + let d = metadata.len() / config.file_size_block_size; + let r = metadata.len() % config.file_size_block_size; + if r == 0 { d } else { d + 1 } + }; + SizeOrDeviceId::Size(display_size(len_adjusted, config)) +} + +pub fn display_size(size: u64, config: &Config) -> String { + human_readable(size, config.size_format) +} + +/// Takes a [`PathData`] struct and returns a cell with a name ready for displaying. +/// +/// This function relies on the following parameters in the provided `&Config`: +/// * `config.quoting_style` to decide how we will escape `name` using [`locale_aware_escape_name`]. +/// * `config.inode` decides whether to display inode numbers beside names using [`get_inode`]. +/// * `config.color` decides whether it's going to color `name` using [`color_name`]. +/// * `config.indicator_style` to append specific characters to `name` using [`classify_file`]. +/// * `config.format` to display symlink targets if `Format::Long`. This function is also +/// responsible for coloring symlink target names if `config.color` is specified. +/// * `config.context` to prepend security context to `name` if compiled with `feat_selinux`. +/// * `config.hyperlink` decides whether to hyperlink the item +/// +/// Note that non-unicode sequences in symlink targets are dealt with using +/// [`std::path::Path::to_string_lossy`]. +#[allow(clippy::cognitive_complexity)] +fn display_item_name( + path: &PathData, + config: &Config, + prefix_context: Option, + more_info: Option, + state: &mut ListState, + current_column: LazyCell usize + '_>>, +) -> DisplayItemName { + // This is our return value. We start by `&path.display_name` and modify it along the way. + let mut name = escape_name_with_locale(path.display_name(), config); + + let is_wrap = + |namelen: usize| config.width != 0 && *current_column + namelen > config.width.into(); + + if config.hyperlink { + name = create_hyperlink(&name, path); + } + + if let Some(style_manager) = &mut state.style_manager { + let len = name.len(); + name = color_name(name, path, style_manager, None, is_wrap(len)); + } + + if config.format != Format::Long { + if let Some(info) = more_info { + let old_name = name; + name = info.into(); + name.push(&old_name); + } + } + + if config.indicator_style != IndicatorStyle::None { + let sym = classify_file(path); + + let char_opt = match config.indicator_style { + IndicatorStyle::Classify => sym, + IndicatorStyle::FileType => { + // Don't append an asterisk. + match sym { + Some('*') => None, + _ => sym, + } + } + IndicatorStyle::Slash => { + // Append only a slash. + match sym { + Some('/') => Some('/'), + _ => None, + } + } + IndicatorStyle::None => None, + }; + + if let Some(c) = char_opt { + name.push(OsStr::new(&c.to_string())); + } + } + + let dired_name_len = if config.dired { name.len() } else { 0 }; + + if config.format == Format::Long + && path.file_type().is_some_and(FileType::is_symlink) + && !path.must_dereference + { + match path.path().read_link() { + Ok(target_path) => { + name.push(" -> "); + + // We might as well color the symlink output after the arrow. + // This makes extra system calls, but provides important information that + // people run `ls -l --color` are very interested in. + if let Some(style_manager) = &mut state.style_manager { + let escaped_target = escape_name_with_locale(target_path.as_os_str(), config); + // We get the absolute path to be able to construct PathData with valid Metadata. + // This is because relative symlinks will fail to get_metadata. + let mut absolute_target = target_path.clone(); + if target_path.is_relative() { + if let Some(parent) = path.path().parent() { + absolute_target = parent.join(absolute_target); + } + } + + match fs::canonicalize(&absolute_target) { + Ok(resolved_target) => { + let target_data = PathData::new( + resolved_target, + None, + target_path.file_name().map(OsStr::to_os_string), + config, + false, + ); + + // Check if the target actually needs coloring + let md_option: Option = target_data + .metadata() + .cloned() + .or_else(|| target_data.p_buf.symlink_metadata().ok()); + let style = style_manager.colors.style_for_path_with_metadata( + &target_data.p_buf, + md_option.as_ref(), + ); + + if style.is_some() { + // Only apply coloring if there's actually a style + name.push(color_name( + escaped_target, + &target_data, + style_manager, + None, + is_wrap(name.len()), + )); + } else { + // For regular files with no coloring, just use plain text + name.push(escaped_target); + } + } + Err(_) => { + name.push( + style_manager.apply_missing_target_style( + escaped_target, + is_wrap(name.len()), + ), + ); + } + } + } else { + // If no coloring is required, we just use target as is. + // Apply the right quoting + name.push(escape_name_with_locale(target_path.as_os_str(), config)); + } + } + Err(err) => { + show!(LsError::IOErrorContext( + path.path().to_path_buf(), + err, + false + )); + } + } + } + + // Prepend the security context to the `name` and adjust `width` in order + // to get correct alignment from later calls to`display_grid()`. + if config.context { + if let Some(pad_count) = prefix_context { + let security_context = if matches!(config.format, Format::Commas) { + path.security_context(config).to_string() + } else { + pad_left(path.security_context(config), pad_count) + }; + + let old_name = name; + name = format!("{security_context} ").into(); + name.push(old_name); + } + } + + DisplayItemName { + displayed: name, + dired_name_len, + } +} + +/// This writes to the [`BufWriter`] `state.out` a single string of the output of `ls -l`. +/// +/// It writes the following keys, in order: +/// * `inode` ([`get_inode`], config-optional) +/// * `permissions` ([`display_permissions`]) +/// * `symlink_count` ([`display_symlink_count`]) +/// * `owner` ([`display_uname`], config-optional) +/// * `group` ([`display_group`], config-optional) +/// * `author` ([`display_uname`], config-optional) +/// * `size / rdev` ([`display_len_or_rdev`]) +/// * `system_time` ([`display_date`]) +/// * `item_name` ([`display_item_name`]) +/// +/// This function needs to display information in columns: +/// * permissions and `system_time` are already guaranteed to be pre-formatted in fixed length. +/// * `item_name` is the last column and is left-aligned. +/// * Everything else needs to be padded using [`pad_left`]. +/// +/// That's why we have the parameters: +/// ```txt +/// longest_link_count_len: usize, +/// longest_uname_len: usize, +/// longest_group_len: usize, +/// longest_context_len: usize, +/// longest_size_len: usize, +/// ``` +/// that decide the maximum possible character count of each field. +#[allow(clippy::write_literal)] +#[allow(clippy::cognitive_complexity)] +fn display_item_long( + item: &PathData, + padding: &PaddingCollection, + config: &Config, + state: &mut ListState, + dired: &mut DiredOutput, + quoted: bool, +) -> UResult<()> { + let mut output_display: Vec = Vec::with_capacity(128); + + // apply normal color to non filename outputs + if let Some(style_manager) = &mut state.style_manager { + output_display.extend(style_manager.apply_normal().as_bytes()); + } + if config.dired { + output_display.extend(b" "); + } + if let Some(md) = item.metadata() { + #[cfg(any(not(unix), target_os = "android", target_os = "macos"))] + // TODO: See how Mac should work here + let is_acl_set = false; + #[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] + let is_acl_set = has_acl(item.path()); + output_display.extend(display_permissions(md, true).as_bytes()); + if item.security_context(config).len() > 1 { + // GNU `ls` uses a "." character to indicate a file with a security context, + // but not other alternate access method. + output_display.extend(b"."); + } else if is_acl_set { + output_display.extend(b"+"); + } else { + output_display.extend(b" "); + } + + output_display.extend_pad_left(&display_symlink_count(md), padding.link_count); + + if config.long.owner { + output_display.extend(b" "); + output_display.extend_pad_right(display_uname(md, config, state), padding.uname); + } + + if config.long.group { + output_display.extend(b" "); + output_display.extend_pad_right(display_group(md, config, state), padding.group); + } + + if config.context { + output_display.extend(b" "); + output_display.extend_pad_right(item.security_context(config), padding.context); + } + + // Author is only different from owner on GNU/Hurd, so we reuse + // the owner, since GNU/Hurd is not currently supported by Rust. + if config.long.author { + output_display.extend(b" "); + output_display.extend_pad_right(display_uname(md, config, state), padding.uname); + } + + match display_len_or_rdev(md, config) { + SizeOrDeviceId::Size(size) => { + output_display.extend(b" "); + output_display.extend_pad_left(&size, padding.size); + } + SizeOrDeviceId::Device(major, minor) => { + output_display.extend(b" "); + output_display.extend_pad_left( + &major, + #[cfg(not(unix))] + 0usize, + #[cfg(unix)] + padding.major.max( + padding + .size + .saturating_sub(padding.minor.saturating_add(2usize)), + ), + ); + output_display.extend(b", "); + output_display.extend_pad_left( + &minor, + #[cfg(not(unix))] + 0usize, + #[cfg(unix)] + padding.minor, + ); + } + } + + output_display.extend(b" "); + display_date(md, config, state, &mut output_display)?; + output_display.extend(b" "); + + let item_display = display_item_name( + item, + config, + None, + None, + state, + LazyCell::new(Box::new(|| { + ansi_width(&String::from_utf8_lossy(&output_display)) + })), + ); + + let needs_space = quoted && !os_str_starts_with(&item_display.displayed, b"'"); + + if config.dired { + let mut dired_name_len = item_display.dired_name_len; + if needs_space { + dired_name_len += 1; + } + let displayed_len = item_display.displayed.len() + usize::from(needs_space); + update_dired_for_item( + dired, + output_display.len(), + displayed_len, + dired_name_len, + config.line_ending, + ); + } + + let item_name = item_display.displayed; + let displayed_item = if needs_space { + let mut ret: OsString = " ".into(); + ret.push(&item_name); + ret + } else { + item_name + }; + + write_os_str(&mut output_display, &displayed_item)?; + output_display.extend(config.line_ending.to_string().as_bytes()); + } else { + #[cfg(unix)] + let leading_char = { + if let Some(ft) = item.file_type() { + if ft.is_char_device() { + "c" + } else if ft.is_block_device() { + "b" + } else if ft.is_symlink() { + "l" + } else if ft.is_dir() { + "d" + } else { + "-" + } + } else if item.is_dangling_link() { + "l" + } else { + "-" + } + }; + #[cfg(not(unix))] + let leading_char = { + if let Some(ft) = item.file_type() { + if ft.is_symlink() { + "l" + } else if ft.is_dir() { + "d" + } else { + "-" + } + } else if item.is_dangling_link() { + "l" + } else { + "-" + } + }; + + output_display.extend(leading_char.as_bytes()); + output_display.extend(b"?????????"); + if item.security_context(config).len() > 1 { + // GNU `ls` uses a "." character to indicate a file with a security context, + // but not other alternate access method. + output_display.extend(b"."); + } + output_display.extend(b" "); + output_display.extend_pad_left("?", padding.link_count); + + if config.long.owner { + output_display.extend(b" "); + output_display.extend_pad_right("?", padding.uname); + } + + if config.long.group { + output_display.extend(b" "); + output_display.extend_pad_right("?", padding.group); + } + + if config.context { + output_display.extend(b" "); + output_display.extend_pad_right(item.security_context(config), padding.context); + } + + // Author is only different from owner on GNU/Hurd, so we reuse + // the owner, since GNU/Hurd is not currently supported by Rust. + if config.long.author { + output_display.extend(b" "); + output_display.extend_pad_right("?", padding.uname); + } + + let displayed_item = display_item_name( + item, + config, + None, + None, + state, + LazyCell::new(Box::new(|| { + ansi_width(&String::from_utf8_lossy(&output_display)) + })), + ); + let date_len = 12; + + output_display.extend(b" "); + output_display.extend_pad_left("?", padding.size); + output_display.extend(b" "); + output_display.extend_pad_left("?", date_len); + output_display.extend(b" "); + + if config.dired { + update_dired_for_item( + dired, + output_display.len(), + displayed_item.displayed.len(), + displayed_item.dired_name_len, + config.line_ending, + ); + } + let displayed_item = displayed_item.displayed; + write_os_str(&mut output_display, &displayed_item)?; + output_display.extend(config.line_ending.to_string().as_bytes()); + } + state.out.write_all(&output_display)?; + + Ok(()) +} + +fn classify_file(path: &PathData) -> Option { + let file_type = path.file_type()?; + + if file_type.is_dir() { + Some('/') + } else if file_type.is_symlink() { + Some('@') + } else { + #[cfg(unix)] + { + if file_type.is_socket() { + Some('=') + } else if file_type.is_fifo() { + Some('|') + // Safe unwrapping if the file was removed between listing and display + // See https://github.com/uutils/coreutils/issues/5371 + } else if path.is_executable_file() { + Some('*') + } else { + None + } + } + #[cfg(not(unix))] + None + } +} + +fn create_hyperlink(name: &OsStr, path: &PathData) -> OsString { + let hostname = hostname::get().unwrap_or_else(|_| OsString::from("")); + let hostname = hostname.to_string_lossy(); + + let absolute_path = fs::canonicalize(path.path()).unwrap_or_default(); + + // Get bytes for URL encoding in a cross-platform way + let absolute_path_bytes = os_str_as_bytes_lossy(absolute_path.as_os_str()); + + // a set of safe ASCII bytes that don't need encoding + #[cfg(not(target_os = "windows"))] + let unencoded_bytes = b"_-.~/"; + #[cfg(target_os = "windows")] + let unencoded_bytes = b"_-.~/\\:"; + + // Encode at byte level to properly handle UTF-8 sequences and preserve invalid UTF-8 + let full_encoded_path: String = absolute_path_bytes + .iter() + .map(|&b: &u8| { + if b.is_ascii_alphanumeric() || unencoded_bytes.contains(&b) { + (b as char).to_string() + } else { + format!("%{b:02x}") + } + }) + .collect(); + + // OSC 8 hyperlink format: \x1b]8;;URL\x1b\\TEXT\x1b]8;;\x1b\\ + // \x1b = ESC, \x1b\\ = ESC backslash + let mut ret: OsString = format!("\x1b]8;;file://{hostname}{full_encoded_path}\x1b\\").into(); + ret.push(name); + ret.push("\x1b]8;;\x1b\\"); + + ret +} + +fn is_hidden(file_path: &DirEntry) -> bool { + #[cfg(windows)] + { + let metadata = file_path.metadata().unwrap(); + let attr = metadata.file_attributes(); + (attr & 0x2) > 0 + } + #[cfg(not(windows))] + { + file_path.file_name().as_encoded_bytes().starts_with(b".") + } +} + +fn update_dired_for_item( + dired: &mut DiredOutput, + output_display_len: usize, + displayed_len: usize, + dired_name_len: usize, + line_ending: LineEnding, +) { + let line_len = calculate_line_len(output_display_len, displayed_len, line_ending); + dired::calculate_and_update_positions(dired, output_display_len, dired_name_len, line_len); +} + +#[cfg(unix)] +fn display_symlink_count(metadata: &Metadata) -> String { + metadata.nlink().to_string() +} + +#[cfg(unix)] +fn display_inode(metadata: &Metadata) -> String { + get_inode(metadata) +} + +#[cfg(unix)] +fn calculate_padding_collection( + items: &[PathData], + config: &Config, + state: &mut ListState, +) -> PaddingCollection { + let mut padding_collections = PaddingCollection { + inode: 1, + link_count: 1, + uname: 1, + group: 1, + context: 1, + size: 1, + major: 1, + minor: 1, + block_size: 1, + }; + + for item in items { + #[cfg(unix)] + if config.inode { + let inode_len = if let Some(md) = item.metadata() { + display_inode(md).len() + } else { + continue; + }; + padding_collections.inode = inode_len.max(padding_collections.inode); + } + + if config.alloc_size { + if let Some(md) = item.metadata() { + let block_size_len = display_size(get_block_size(md, config), config).len(); + padding_collections.block_size = block_size_len.max(padding_collections.block_size); + } + } + + if config.format == Format::Long { + let context_len = item.security_context(config).len(); + let (link_count_len, uname_len, group_len, size_len, major_len, minor_len) = + display_dir_entry_size(item, config, state); + padding_collections.link_count = link_count_len.max(padding_collections.link_count); + padding_collections.uname = uname_len.max(padding_collections.uname); + padding_collections.group = group_len.max(padding_collections.group); + if config.context { + padding_collections.context = context_len.max(padding_collections.context); + } + + // correctly align columns when some files have capabilities/ACLs and others do not + { + #[cfg(any(not(unix), target_os = "android", target_os = "macos"))] + // TODO: See how Mac should work here + let is_acl_set = false; + #[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] + let is_acl_set = has_acl(item.display_name()); + if context_len > 1 || is_acl_set { + padding_collections.link_count += 1; + } + } + + if items.len() == 1usize { + padding_collections.size = 0usize; + padding_collections.major = 0usize; + padding_collections.minor = 0usize; + } else { + padding_collections.major = major_len.max(padding_collections.major); + padding_collections.minor = minor_len.max(padding_collections.minor); + padding_collections.size = size_len + .max(padding_collections.size) + .max(padding_collections.major); + } + } + } + + padding_collections +} + +#[cfg(not(unix))] +fn display_symlink_count(_metadata: &Metadata) -> String { + // Currently not sure of how to get this on Windows, so I'm punting. + // Git Bash looks like it may do the same thing. + String::from("1") +} + +#[cfg(not(unix))] +fn calculate_padding_collection( + items: &[PathData], + config: &Config, + state: &mut ListState, +) -> PaddingCollection { + let mut padding_collections = PaddingCollection { + link_count: 1, + uname: 1, + group: 1, + context: 1, + size: 1, + block_size: 1, + }; + + for item in items { + if config.alloc_size { + if let Some(md) = item.metadata() { + let block_size_len = display_size(get_block_size(md, config), config).len(); + padding_collections.block_size = block_size_len.max(padding_collections.block_size); + } + } + + let context_len = item.security_context(config).len(); + let (link_count_len, uname_len, group_len, size_len, _major_len, _minor_len) = + display_dir_entry_size(item, config, state); + padding_collections.link_count = link_count_len.max(padding_collections.link_count); + padding_collections.uname = uname_len.max(padding_collections.uname); + padding_collections.group = group_len.max(padding_collections.group); + if config.context { + padding_collections.context = context_len.max(padding_collections.context); + } + padding_collections.size = size_len.max(padding_collections.size); + } + + padding_collections +} + +fn os_str_starts_with(haystack: &OsStr, needle: &[u8]) -> bool { + os_str_as_bytes_lossy(haystack).starts_with(needle) +} + +fn write_os_str(writer: &mut W, string: &OsStr) -> std::io::Result<()> { + writer.write_all(&os_str_as_bytes_lossy(string)) +} diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 507689eee41..913bc04abd8 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -13,168 +13,51 @@ use std::borrow::Cow; use std::cell::RefCell; #[cfg(unix)] use std::os::unix::fs::{FileTypeExt, MetadataExt}; -#[cfg(windows)] -use std::os::windows::fs::MetadataExt; use std::{ - cell::{LazyCell, OnceCell}, + cell::OnceCell, cmp::Reverse, ffi::{OsStr, OsString}, - fmt::Write as _, fs::{self, DirEntry, FileType, Metadata, ReadDir}, - io::{BufWriter, ErrorKind, IsTerminal, Stdout, Write, stdout}, - iter, - num::IntErrorKind, + io::{BufWriter, ErrorKind, Stdout, Write, stdout}, ops::RangeInclusive, path::{Path, PathBuf}, time::{Duration, SystemTime, UNIX_EPOCH}, }; -use ansi_width::ansi_width; use clap::{ Arg, ArgAction, Command, builder::{NonEmptyStringValueParser, PossibleValue, ValueParser}, }; -use glob::{MatchOptions, Pattern}; -use lscolors::{Colorable, LsColors}; -use term_grid::{DEFAULT_SEPARATOR_SIZE, Direction, Filling, Grid, GridOptions, SPACES_IN_TAB}; +use lscolors::Colorable; use thiserror::Error; -#[cfg(unix)] -use uucore::entries; -#[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] -use uucore::fsxattr::has_acl; #[cfg(unix)] use uucore::libc::{S_IXGRP, S_IXOTH, S_IXUSR}; -#[cfg(any( - target_os = "linux", - target_os = "macos", - target_os = "android", - target_os = "ios", - target_os = "freebsd", - target_os = "dragonfly", - target_os = "netbsd", - target_os = "openbsd", - target_os = "illumos", - target_os = "solaris" -))] -use uucore::libc::{dev_t, major, minor}; use uucore::{ display::Quotable, error::{UError, UResult, set_exit_code}, - format::human::{SizeFormat, human_readable}, format_usage, fs::FileInformation, - fs::display_permissions, - fsext::{MetadataTimeField, metadata_get_time}, - line_ending::LineEnding, + fsext::metadata_get_time, os_str_as_bytes_lossy, - parser::parse_glob, - 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, - time::{FormatSystemTimeFallback, format, format_system_time}, - translate, + show, translate, version_cmp::version_cmp, }; -mod dired; -use dired::{DiredOutput, is_dired_arg_present}; mod colors; -use crate::options::QUOTING_STYLE; -use colors::{LsColorsParseError, StyleManager, color_name, validate_ls_colors_env}; - -pub mod options { - pub mod format { - pub static ONE_LINE: &str = "1"; - pub static LONG: &str = "long"; - pub static COLUMNS: &str = "C"; - pub static ACROSS: &str = "x"; - pub static TAB_SIZE: &str = "tabsize"; - pub static COMMAS: &str = "m"; - pub static LONG_NO_OWNER: &str = "g"; - pub static LONG_NO_GROUP: &str = "o"; - pub static LONG_NUMERIC_UID_GID: &str = "numeric-uid-gid"; - } - - pub mod files { - pub static ALL: &str = "all"; - pub static ALMOST_ALL: &str = "almost-all"; - pub static UNSORTED_ALL: &str = "f"; - } - - pub mod sort { - pub static SIZE: &str = "S"; - pub static TIME: &str = "t"; - pub static NONE: &str = "U"; - pub static VERSION: &str = "v"; - pub static EXTENSION: &str = "X"; - } - - pub mod time { - pub static ACCESS: &str = "u"; - pub static CHANGE: &str = "c"; - } - - pub mod size { - pub static ALLOCATION_SIZE: &str = "size"; - pub static BLOCK_SIZE: &str = "block-size"; - pub static HUMAN_READABLE: &str = "human-readable"; - pub static SI: &str = "si"; - pub static KIBIBYTES: &str = "kibibytes"; - } - - pub mod quoting { - pub static ESCAPE: &str = "escape"; - pub static LITERAL: &str = "literal"; - pub static C: &str = "quote-name"; - } - - pub mod indicator_style { - pub static SLASH: &str = "p"; - pub static FILE_TYPE: &str = "file-type"; - pub static CLASSIFY: &str = "classify"; - } - - pub mod dereference { - pub static ALL: &str = "dereference"; - pub static ARGS: &str = "dereference-command-line"; - pub static DIR_ARGS: &str = "dereference-command-line-symlink-to-dir"; - } +mod config; +mod dired; +mod display; - pub static HELP: &str = "help"; - pub static QUOTING_STYLE: &str = "quoting-style"; - pub static HIDE_CONTROL_CHARS: &str = "hide-control-chars"; - pub static SHOW_CONTROL_CHARS: &str = "show-control-chars"; - pub static WIDTH: &str = "width"; - pub static AUTHOR: &str = "author"; - pub static NO_GROUP: &str = "no-group"; - pub static FORMAT: &str = "format"; - pub static SORT: &str = "sort"; - pub static TIME: &str = "time"; - pub static IGNORE_BACKUPS: &str = "ignore-backups"; - pub static DIRECTORY: &str = "directory"; - pub static INODE: &str = "inode"; - pub static REVERSE: &str = "reverse"; - pub static RECURSIVE: &str = "recursive"; - pub static COLOR: &str = "color"; - pub static PATHS: &str = "paths"; - pub static INDICATOR_STYLE: &str = "indicator-style"; - pub static TIME_STYLE: &str = "time-style"; - pub static FULL_TIME: &str = "full-time"; - pub static HIDE: &str = "hide"; - pub static IGNORE: &str = "ignore"; - pub static CONTEXT: &str = "context"; - pub static GROUP_DIRECTORIES_FIRST: &str = "group-directories-first"; - pub static ZERO: &str = "zero"; - pub static DIRED: &str = "dired"; - pub static HYPERLINK: &str = "hyperlink"; -} +pub use config::{Config, options}; +pub use display::Format; -const DEFAULT_TERM_WIDTH: u16 = 80; -const POSIXLY_CORRECT_BLOCK_SIZE: u64 = 512; -const DEFAULT_BLOCK_SIZE: u64 = 1024; -const DEFAULT_FILE_SIZE_BLOCK_SIZE: u64 = 1; +use colors::StyleManager; +use config::options::QUOTING_STYLE; +use config::{Dereference, Files, Sort}; +use dired::DiredOutput; +use display::{display_items, display_size, should_display, show_dir_name}; #[derive(Error, Debug)] enum LsError { @@ -230,1037 +113,17 @@ impl UError for LsError { } } -#[derive(PartialEq, Eq, Debug)] -pub enum Format { - Columns, - Long, - OneLine, - Across, - Commas, -} - -#[derive(PartialEq, Eq)] -enum Sort { - None, - Name, - Size, - Time, - Version, - Extension, - Width, -} - -#[derive(PartialEq, Eq)] -enum Files { - All, - AlmostAll, - Normal, -} - -fn parse_time_style(options: &clap::ArgMatches) -> Result<(String, Option), LsError> { - // TODO: Using correct locale string is not implemented. - const LOCALE_FORMAT: (&str, Option<&str>) = ("%b %e %H:%M", Some("%b %e %Y")); - - // Convert time_styles references to owned String/option. - #[expect(clippy::unnecessary_wraps, reason = "internal result helper")] - fn ok((recent, older): (&str, Option<&str>)) -> Result<(String, Option), LsError> { - Ok((recent.to_string(), older.map(String::from))) - } - - if let Some(field) = options - .get_one::(options::TIME_STYLE) - .map(ToOwned::to_owned) - .or_else(|| std::env::var("TIME_STYLE").ok()) - { - //If both FULL_TIME and TIME_STYLE are present - //The one added last is dominant - if options.get_flag(options::FULL_TIME) - && options.indices_of(options::FULL_TIME).unwrap().next_back() - > options.indices_of(options::TIME_STYLE).unwrap().next_back() - { - ok((format::FULL_ISO, None)) - } else { - let field = if let Some(field) = field.strip_prefix("posix-") { - // See GNU documentation, set format to "locale" if LC_TIME="POSIX", - // else just strip the prefix and continue (even "posix+FORMAT" is - // supported). - // TODO: This needs to be moved to uucore and handled by icu? - if std::env::var_os("LC_TIME").as_deref() == Some(OsStr::new("POSIX")) - || std::env::var_os("LC_ALL").as_deref() == Some(OsStr::new("POSIX")) - { - return ok(LOCALE_FORMAT); - } - field - } else { - &field - }; - - match field { - "full-iso" => ok((format::FULL_ISO, None)), - "long-iso" => ok((format::LONG_ISO, None)), - // ISO older format needs extra padding. - "iso" => Ok(( - "%m-%d %H:%M".to_string(), - Some(format::ISO.to_string() + " "), - )), - "locale" => ok(LOCALE_FORMAT), - _ => match field.chars().next().unwrap() { - '+' => { - // recent/older formats are (optionally) separated by a newline - let mut it = field[1..].split('\n'); - let recent = it.next().unwrap_or_default(); - let older = it.next(); - match it.next() { - None => ok((recent, older)), - Some(_) => Err(LsError::TimeStyleParseError(String::from(field))), - } - } - _ => Err(LsError::TimeStyleParseError(String::from(field))), - }, - } - } - } else if options.get_flag(options::FULL_TIME) { - ok((format::FULL_ISO, None)) - } else { - ok(LOCALE_FORMAT) - } -} - -enum Dereference { - None, - DirArgs, - Args, - All, -} - -#[derive(PartialEq, Eq)] -enum IndicatorStyle { - None, - Slash, - FileType, - Classify, -} - -#[derive(Clone, Copy, PartialEq, Eq)] -enum LocaleQuoting { - Single, - Double, -} - -pub struct Config { - // Dir and vdir needs access to this field - pub format: Format, - files: Files, - sort: Sort, - recursive: bool, - reverse: bool, - dereference: Dereference, - ignore_patterns: Vec, - size_format: SizeFormat, - directory: bool, - time: MetadataTimeField, - #[cfg(unix)] - inode: bool, - color: Option, - long: LongFormat, - alloc_size: bool, - file_size_block_size: u64, - #[allow(dead_code)] - block_size: u64, // is never read on Windows - width: u16, - // Dir and vdir needs access to this field - pub quoting_style: QuotingStyle, - locale_quoting: Option, - indicator_style: IndicatorStyle, - time_format_recent: String, // Time format for recent dates - time_format_older: Option, // Time format for older dates (optional, if not present, time_format_recent is used) - context: bool, - #[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))] - selinux_supported: bool, - #[cfg(all(feature = "smack", target_os = "linux"))] - smack_supported: bool, - group_directories_first: bool, - line_ending: LineEnding, - dired: bool, - hyperlink: bool, - tab_size: usize, -} - -// Fields that can be removed or added to the long format -struct LongFormat { - author: bool, - group: bool, - owner: bool, - #[cfg(unix)] - numeric_uid_gid: bool, -} - -struct PaddingCollection { - #[cfg(unix)] - inode: usize, - link_count: usize, - uname: usize, - group: usize, - context: usize, - size: usize, - #[cfg(unix)] - major: usize, - #[cfg(unix)] - minor: usize, - block_size: usize, -} - -struct DisplayItemName { - displayed: OsString, - dired_name_len: usize, -} - -/// Extracts the format to display the information based on the options provided. -/// -/// # Returns -/// -/// A tuple containing the Format variant and an Option containing a &'static str -/// which corresponds to the option used to define the format. -fn extract_format(options: &clap::ArgMatches) -> (Format, Option<&'static str>) { - if let Some(format_) = options.get_one::(options::FORMAT) { - ( - match format_.as_str() { - "long" | "verbose" => Format::Long, - "single-column" => Format::OneLine, - "columns" | "vertical" => Format::Columns, - "across" | "horizontal" => Format::Across, - "commas" => Format::Commas, - // below should never happen as clap already restricts the values. - _ => unreachable!("Invalid field for --format"), - }, - Some(options::FORMAT), - ) - } else if options.get_flag(options::format::LONG) { - (Format::Long, Some(options::format::LONG)) - } else if options.get_flag(options::format::ACROSS) { - (Format::Across, Some(options::format::ACROSS)) - } else if options.get_flag(options::format::COMMAS) { - (Format::Commas, Some(options::format::COMMAS)) - } else if options.get_flag(options::format::COLUMNS) { - (Format::Columns, Some(options::format::COLUMNS)) - } else if stdout().is_terminal() { - (Format::Columns, None) - } else { - (Format::OneLine, None) - } -} - -/// Extracts the type of files to display -/// -/// # Returns -/// -/// A Files variant representing the type of files to display. -fn extract_files(options: &clap::ArgMatches) -> Files { - let get_last_index = |flag: &str| -> usize { - if options.value_source(flag) == Some(clap::parser::ValueSource::CommandLine) { - options.index_of(flag).unwrap_or(0) - } else { - 0 - } - }; - - let all_index = get_last_index(options::files::ALL); - let almost_all_index = get_last_index(options::files::ALMOST_ALL); - let unsorted_all_index = get_last_index(options::files::UNSORTED_ALL); - - let max_index = all_index.max(almost_all_index).max(unsorted_all_index); - - if max_index == 0 { - Files::Normal - } else if max_index == almost_all_index { - Files::AlmostAll - } else { - // Either -a or -f wins, both show all files - Files::All - } -} - -/// Extracts the sorting method to use based on the options provided. -/// -/// # Returns -/// -/// A Sort variant representing the sorting method to use. -fn extract_sort(options: &clap::ArgMatches) -> Sort { - let get_last_index = |flag: &str| -> usize { - if options.value_source(flag) == Some(clap::parser::ValueSource::CommandLine) { - options.index_of(flag).unwrap_or(0) - } else { - 0 - } - }; - - let sort_index = options - .get_one::(options::SORT) - .and_then(|_| options.indices_of(options::SORT)) - .map_or(0, |mut indices| indices.next_back().unwrap_or(0)); - let time_index = get_last_index(options::sort::TIME); - let size_index = get_last_index(options::sort::SIZE); - let none_index = get_last_index(options::sort::NONE); - let version_index = get_last_index(options::sort::VERSION); - let extension_index = get_last_index(options::sort::EXTENSION); - let unsorted_all_index = get_last_index(options::files::UNSORTED_ALL); - - let max_sort_index = sort_index - .max(time_index) - .max(size_index) - .max(none_index) - .max(version_index) - .max(extension_index) - .max(unsorted_all_index); - - match max_sort_index { - 0 => { - // No sort flags specified, use default behavior - if !options.get_flag(options::format::LONG) - && (options.get_flag(options::time::ACCESS) - || options.get_flag(options::time::CHANGE) - || options.get_one::(options::TIME).is_some()) - { - Sort::Time - } else { - Sort::Name - } - } - idx if idx == unsorted_all_index || idx == none_index => Sort::None, - idx if idx == sort_index => { - if let Some(field) = options.get_one::(options::SORT) { - match field.as_str() { - "none" => Sort::None, - "name" => Sort::Name, - "time" => Sort::Time, - "size" => Sort::Size, - "version" => Sort::Version, - "extension" => Sort::Extension, - "width" => Sort::Width, - _ => unreachable!("Invalid field for --sort"), - } - } else { - Sort::Name - } - } - idx if idx == time_index => Sort::Time, - idx if idx == size_index => Sort::Size, - idx if idx == version_index => Sort::Version, - idx if idx == extension_index => Sort::Extension, - _ => Sort::Name, - } -} - -/// Extracts the time to use based on the options provided. -/// -/// # Returns -/// -/// A `MetadataTimeField` variant representing the time to use. -fn extract_time(options: &clap::ArgMatches) -> MetadataTimeField { - if let Some(field) = options.get_one::(options::TIME) { - field.as_str().into() - } else if options.get_flag(options::time::ACCESS) { - MetadataTimeField::Access - } else if options.get_flag(options::time::CHANGE) { - MetadataTimeField::Change - } else { - MetadataTimeField::Modification - } -} - -/// Some env variables can be passed -/// For now, we are only verifying if empty or not and known for `TERM` -fn is_color_compatible_term() -> bool { - let is_term_set = std::env::var("TERM").is_ok(); - let is_colorterm_set = std::env::var("COLORTERM").is_ok(); - - let term = std::env::var("TERM").unwrap_or_default(); - let colorterm = std::env::var("COLORTERM").unwrap_or_default(); - - // Search function in the TERM struct to manage the wildcards - let term_matches = |term: &str| -> bool { - uucore::colors::TERMS.iter().any(|&pattern| { - term == pattern - || (pattern.ends_with('*') && term.starts_with(&pattern[..pattern.len() - 1])) - }) - }; - - if is_term_set && term.is_empty() && is_colorterm_set && colorterm.is_empty() { - return false; - } - - if !term.is_empty() && !term_matches(&term) { - return false; - } - true -} - -/// Extracts the color option to use based on the options provided. -/// -/// # Returns -/// -/// A boolean representing whether or not to use color. -fn extract_color(options: &clap::ArgMatches) -> bool { - if !is_color_compatible_term() { - return false; - } - - let get_last_index = |flag: &str| -> usize { - if options.value_source(flag) == Some(clap::parser::ValueSource::CommandLine) { - options.index_of(flag).unwrap_or(0) - } else { - 0 - } - }; - - let color_index = options - .get_one::(options::COLOR) - .and_then(|_| options.indices_of(options::COLOR)) - .map_or(0, |mut indices| indices.next_back().unwrap_or(0)); - let unsorted_all_index = get_last_index(options::files::UNSORTED_ALL); - - let color_enabled = match options.get_one::(options::COLOR) { - None => options.contains_id(options::COLOR), - Some(val) => match val.as_str() { - "" | "always" | "yes" | "force" => true, - "auto" | "tty" | "if-tty" => stdout().is_terminal(), - /* "never" | "no" | "none" | */ _ => false, - }, - }; - - // If --color was explicitly specified, always honor it regardless of -f - // Otherwise, if -f is present without explicit color, disable color - if color_index > 0 { - // Color was explicitly specified - color_enabled - } else if unsorted_all_index > 0 { - // -f present without explicit color, disable implicit color - false - } else { - color_enabled - } -} - -/// Extracts the hyperlink option to use based on the options provided. -/// -/// # Returns -/// -/// A boolean representing whether to hyperlink files. -fn extract_hyperlink(options: &clap::ArgMatches) -> bool { - let hyperlink = options - .get_one::(options::HYPERLINK) - .unwrap() - .as_str(); - - match hyperlink { - "always" | "yes" | "force" => true, - "auto" | "tty" | "if-tty" => stdout().is_terminal(), - "never" | "no" | "none" => false, - _ => unreachable!("should be handled by clap"), - } -} - -/// Match the argument given to --quoting-style or the [`QUOTING_STYLE`] env variable. -/// -/// # Arguments -/// -/// * `style`: the actual argument string -/// * `show_control` - A boolean value representing whether to show control characters. -/// -/// # Returns -/// -/// * An option with None if the style string is invalid, or a `QuotingStyle` wrapped in `Some`. -struct QuotingStyleSpec { - style: QuotingStyle, - fixed_control: bool, - locale: Option, -} - -impl QuotingStyleSpec { - fn new(style: QuotingStyle) -> Self { - Self { - style, - fixed_control: false, - locale: None, - } - } - - fn with_locale(style: QuotingStyle, locale: LocaleQuoting) -> Self { - Self { - style, - fixed_control: true, - locale: Some(locale), - } - } -} - -fn match_quoting_style_name( - style: &str, - show_control: bool, -) -> Option<(QuotingStyle, Option)> { - let spec = match style { - "literal" => QuotingStyleSpec::new(QuotingStyle::Literal { - show_control: false, - }), - "shell" => QuotingStyleSpec::new(QuotingStyle::SHELL), - "shell-always" => QuotingStyleSpec::new(QuotingStyle::SHELL_QUOTE), - "shell-escape" => QuotingStyleSpec::new(QuotingStyle::SHELL_ESCAPE), - "shell-escape-always" => QuotingStyleSpec::new(QuotingStyle::SHELL_ESCAPE_QUOTE), - "c" => QuotingStyleSpec::new(QuotingStyle::C_DOUBLE), - "escape" => QuotingStyleSpec::new(QuotingStyle::C_NO_QUOTES), - "locale" => QuotingStyleSpec { - style: QuotingStyle::Literal { - show_control: false, - }, - fixed_control: true, - locale: Some(LocaleQuoting::Single), - }, - "clocale" => QuotingStyleSpec::with_locale(QuotingStyle::C_DOUBLE, LocaleQuoting::Double), - _ => return None, - }; - - let style = if spec.fixed_control { - spec.style - } else { - spec.style.show_control(show_control) - }; - - Some((style, spec.locale)) -} - -/// Extracts the quoting style to use based on the options provided. -/// If no options are given, it looks if a default quoting style is provided -/// through the [`QUOTING_STYLE`] environment variable. -/// -/// # Arguments -/// -/// * `options` - A reference to a [`clap::ArgMatches`] object containing command line arguments. -/// * `show_control` - A boolean value representing whether or not to show control characters. -/// -/// # Returns -/// -/// A [`QuotingStyle`] variant representing the quoting style to use. -fn extract_quoting_style( - options: &clap::ArgMatches, - show_control: bool, -) -> (QuotingStyle, Option) { - let opt_quoting_style = options.get_one::(QUOTING_STYLE); - - if let Some(style) = opt_quoting_style { - match match_quoting_style_name(style, show_control) { - Some(pair) => pair, - None => unreachable!("Should have been caught by Clap"), - } - } else if options.get_flag(options::quoting::LITERAL) { - (QuotingStyle::Literal { show_control }, None) - } else if options.get_flag(options::quoting::ESCAPE) { - (QuotingStyle::C_NO_QUOTES, None) - } else if options.get_flag(options::quoting::C) { - (QuotingStyle::C_DOUBLE, None) - } else if options.get_flag(options::DIRED) { - (QuotingStyle::Literal { show_control }, None) - } else { - // If set, the QUOTING_STYLE environment variable specifies a default style. - if let Ok(style) = std::env::var("QUOTING_STYLE") { - match match_quoting_style_name(style.as_str(), show_control) { - Some(pair) => return pair, - None => eprintln!( - "{}", - translate!("ls-invalid-quoting-style", "program" => std::env::args().next().unwrap_or_else(|| "ls".to_string()), "style" => style.clone()) - ), - } - } - - // By default, `ls` uses Shell escape quoting style when writing to a terminal file - // descriptor and Literal otherwise. - if stdout().is_terminal() { - (QuotingStyle::SHELL_ESCAPE.show_control(show_control), None) - } else { - (QuotingStyle::Literal { show_control }, None) - } - } -} - -/// Extracts the indicator style to use based on the options provided. -/// -/// # Returns -/// -/// An [`IndicatorStyle`] variant representing the indicator style to use. -fn extract_indicator_style(options: &clap::ArgMatches) -> IndicatorStyle { - if let Some(field) = options.get_one::(options::INDICATOR_STYLE) { - match field.as_str() { - "none" => IndicatorStyle::None, - "file-type" => IndicatorStyle::FileType, - "classify" => IndicatorStyle::Classify, - "slash" => IndicatorStyle::Slash, - &_ => IndicatorStyle::None, - } - } else if let Some(field) = options.get_one::(options::indicator_style::CLASSIFY) { - match field.as_str() { - "never" | "no" | "none" => IndicatorStyle::None, - "always" | "yes" | "force" => IndicatorStyle::Classify, - "auto" | "tty" | "if-tty" => { - if stdout().is_terminal() { - IndicatorStyle::Classify - } else { - IndicatorStyle::None - } - } - &_ => IndicatorStyle::None, - } - } else if options.get_flag(options::indicator_style::SLASH) { - IndicatorStyle::Slash - } else if options.get_flag(options::indicator_style::FILE_TYPE) { - IndicatorStyle::FileType - } else { - IndicatorStyle::None - } -} - -/// Parses the width value from either the command line arguments or the environment variables. -fn parse_width(width_match: Option<&String>) -> Result { - let parse_width_from_args = |s: &str| -> Result { - let radix = if s.starts_with('0') && s.len() > 1 { - 8 - } else { - 10 - }; - match u16::from_str_radix(s, radix) { - Ok(x) => Ok(x), - Err(e) => match e.kind() { - IntErrorKind::PosOverflow => Ok(u16::MAX), - _ => Err(LsError::InvalidLineWidth(s.into())), - }, - } - }; - - let parse_width_from_env = |columns: OsString| { - if let Some(columns) = columns.to_str().and_then(|s| s.parse().ok()) { - columns - } else { - show_error!( - "{}", - translate!("ls-invalid-columns-width", "width" => columns.quote()) - ); - DEFAULT_TERM_WIDTH - } - }; +#[uucore::main] +pub fn uumain(args: impl uucore::Args) -> UResult<()> { + let matches = uucore::clap_localization::handle_clap_result_with_exit_code(uu_app(), args, 2)?; - let calculate_term_size = || match terminal_size::terminal_size() { - Some((width, _)) => width.0, - None => DEFAULT_TERM_WIDTH, - }; + let config = Config::from(&matches)?; - let ret = match width_match { - Some(x) => parse_width_from_args(x)?, - None => match std::env::var_os("COLUMNS") { - Some(columns) => parse_width_from_env(columns), - None => calculate_term_size(), - }, - }; + let locs = matches + .get_many::(options::PATHS) + .map_or_else(|| vec![Path::new(".")], |v| v.map(Path::new).collect()); - Ok(ret) -} - -impl Config { - #[allow(clippy::cognitive_complexity)] - pub fn from(options: &clap::ArgMatches) -> UResult { - let context = options.get_flag(options::CONTEXT); - let (mut format, opt) = extract_format(options); - let files = extract_files(options); - - // The -o, -n and -g options are tricky. They cannot override with each - // other because it's possible to combine them. For example, the option - // -og should hide both owner and group. Furthermore, they are not - // reset if -l or --format=long is used. So these should just show the - // group: -gl or "-g --format=long". Finally, they are also not reset - // when switching to a different format option in-between like this: - // -ogCl or "-og --format=vertical --format=long". - // - // -1 has a similar issue: it does nothing if the format is long. This - // actually makes it distinct from the --format=singe-column option, - // which always applies. - // - // The idea here is to not let these options override with the other - // options, but manually whether they have an index that's greater than - // the other format options. If so, we set the appropriate format. - if format != Format::Long { - let idx = opt - .and_then(|opt| options.indices_of(opt).map(|x| x.max().unwrap())) - .unwrap_or(0); - if [ - options::format::LONG_NO_OWNER, - options::format::LONG_NO_GROUP, - options::format::LONG_NUMERIC_UID_GID, - options::FULL_TIME, - ] - .iter() - .filter_map(|opt| { - if options.value_source(opt) == Some(clap::parser::ValueSource::CommandLine) { - options.indices_of(opt) - } else { - None - } - }) - .flatten() - .any(|i| i >= idx) - { - format = Format::Long; - } else if let Some(mut indices) = options.indices_of(options::format::ONE_LINE) { - if options.value_source(options::format::ONE_LINE) - == Some(clap::parser::ValueSource::CommandLine) - && indices.any(|i| i > idx) - { - format = Format::OneLine; - } - } - } - - let sort = extract_sort(options); - let time = extract_time(options); - let mut needs_color = extract_color(options); - let hyperlink = extract_hyperlink(options); - - let opt_block_size = options.get_one::(options::size::BLOCK_SIZE); - let opt_si = opt_block_size.is_some() - && options - .get_one::(options::size::BLOCK_SIZE) - .unwrap() - .eq("si") - || options.get_flag(options::size::SI); - let opt_hr = (opt_block_size.is_some() - && options - .get_one::(options::size::BLOCK_SIZE) - .unwrap() - .eq("human-readable")) - || options.get_flag(options::size::HUMAN_READABLE); - let opt_kb = options.get_flag(options::size::KIBIBYTES); - - let size_format = if opt_si { - SizeFormat::Decimal - } else if opt_hr { - SizeFormat::Binary - } else { - SizeFormat::Bytes - }; - - let env_var_blocksize = std::env::var_os("BLOCKSIZE"); - let env_var_block_size = std::env::var_os("BLOCK_SIZE"); - let env_var_ls_block_size = std::env::var_os("LS_BLOCK_SIZE"); - let env_var_posixly_correct = std::env::var_os("POSIXLY_CORRECT"); - let mut is_env_var_blocksize = false; - - let raw_block_size = if let Some(opt_block_size) = opt_block_size { - OsString::from(opt_block_size) - } else if let Some(env_var_ls_block_size) = env_var_ls_block_size { - env_var_ls_block_size - } else if let Some(env_var_block_size) = env_var_block_size { - env_var_block_size - } else if let Some(env_var_blocksize) = env_var_blocksize { - is_env_var_blocksize = true; - env_var_blocksize - } else { - OsString::from("") - }; - - let (file_size_block_size, block_size) = if !opt_si && !opt_hr && !raw_block_size.is_empty() - { - if let Ok(size) = parse_size_non_zero_u64(&raw_block_size.to_string_lossy()) { - 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), - (false, true) => { - // --block-size overrides -k - if opt_block_size.is_some() { - (size, size) - } else { - (size, DEFAULT_BLOCK_SIZE) - } - } - (false, false) => (size, size), - } - } else { - // only fail if invalid block size was specified with --block-size, - // ignore invalid block size from env vars - if let Some(invalid_block_size) = opt_block_size { - return Err(Box::new(LsError::BlockSizeParseError( - invalid_block_size.clone(), - ))); - } - if is_env_var_blocksize { - (DEFAULT_FILE_SIZE_BLOCK_SIZE, DEFAULT_BLOCK_SIZE) - } else { - (DEFAULT_BLOCK_SIZE, DEFAULT_BLOCK_SIZE) - } - } - } else if env_var_posixly_correct.is_some() { - if opt_kb { - (DEFAULT_FILE_SIZE_BLOCK_SIZE, DEFAULT_BLOCK_SIZE) - } else { - (DEFAULT_FILE_SIZE_BLOCK_SIZE, POSIXLY_CORRECT_BLOCK_SIZE) - } - } else if opt_si { - (DEFAULT_FILE_SIZE_BLOCK_SIZE, 1000) - } else { - (DEFAULT_FILE_SIZE_BLOCK_SIZE, DEFAULT_BLOCK_SIZE) - }; - - let long = { - let author = options.get_flag(options::AUTHOR); - let group = !options.get_flag(options::NO_GROUP) - && !options.get_flag(options::format::LONG_NO_GROUP); - let owner = !options.get_flag(options::format::LONG_NO_OWNER); - #[cfg(unix)] - let numeric_uid_gid = options.get_flag(options::format::LONG_NUMERIC_UID_GID); - LongFormat { - author, - group, - owner, - #[cfg(unix)] - numeric_uid_gid, - } - }; - let width = parse_width(options.get_one::(options::WIDTH))?; - - #[allow(clippy::needless_bool)] - let mut show_control = if options.get_flag(options::HIDE_CONTROL_CHARS) { - false - } else if options.get_flag(options::SHOW_CONTROL_CHARS) { - true - } else { - !stdout().is_terminal() - }; - - let (mut quoting_style, mut locale_quoting) = extract_quoting_style(options, show_control); - let indicator_style = extract_indicator_style(options); - // Only parse the value to "--time-style" if it will become relevant. - let dired = options.get_flag(options::DIRED); - let (time_format_recent, time_format_older) = if format == Format::Long || dired { - parse_time_style(options)? - } else { - Default::default() - }; - - let mut ignore_patterns: Vec = Vec::new(); - - if options.get_flag(options::IGNORE_BACKUPS) { - ignore_patterns.push(Pattern::new("*~").unwrap()); - ignore_patterns.push(Pattern::new(".*~").unwrap()); - } - - for pattern in options - .get_many::(options::IGNORE) - .into_iter() - .flatten() - { - if let Ok(p) = parse_glob::from_str(pattern) { - ignore_patterns.push(p); - } else { - show_warning!( - "{}", - translate!("ls-invalid-ignore-pattern", "pattern" => pattern.quote()) - ); - } - } - - if files == Files::Normal { - for pattern in options - .get_many::(options::HIDE) - .into_iter() - .flatten() - { - if let Ok(p) = parse_glob::from_str(pattern) { - ignore_patterns.push(p); - } else { - show_warning!( - "{}", - translate!("ls-invalid-hide-pattern", "pattern" => pattern.quote()) - ); - } - } - } - - // According to ls info page, `--zero` implies the following flags: - // - `--show-control-chars` - // - `--format=single-column` - // - `--color=none` - // - `--quoting-style=literal` - // Current GNU ls implementation allows `--zero` Behavior to be - // overridden by later flags. - let zero_formats_opts = [ - options::format::ACROSS, - options::format::COLUMNS, - options::format::COMMAS, - options::format::LONG, - options::format::LONG_NO_GROUP, - options::format::LONG_NO_OWNER, - options::format::LONG_NUMERIC_UID_GID, - options::format::ONE_LINE, - options::FORMAT, - ]; - let zero_colors_opts = [options::COLOR]; - let zero_show_control_opts = [options::HIDE_CONTROL_CHARS, options::SHOW_CONTROL_CHARS]; - let zero_quoting_style_opts = [ - QUOTING_STYLE, - options::quoting::C, - options::quoting::ESCAPE, - options::quoting::LITERAL, - ]; - let get_last = |flag: &str| -> usize { - if options.value_source(flag) == Some(clap::parser::ValueSource::CommandLine) { - options.index_of(flag).unwrap_or(0) - } else { - 0 - } - }; - if get_last(options::ZERO) - > zero_formats_opts - .into_iter() - .map(get_last) - .max() - .unwrap_or(0) - { - format = if format == Format::Long { - format - } else { - Format::OneLine - }; - } - if get_last(options::ZERO) - > zero_colors_opts - .into_iter() - .map(get_last) - .max() - .unwrap_or(0) - { - needs_color = false; - } - if get_last(options::ZERO) - > zero_show_control_opts - .into_iter() - .map(get_last) - .max() - .unwrap_or(0) - { - show_control = true; - } - if get_last(options::ZERO) - > zero_quoting_style_opts - .into_iter() - .map(get_last) - .max() - .unwrap_or(0) - { - quoting_style = QuotingStyle::Literal { show_control }; - locale_quoting = None; - } - - if needs_color { - if let Err(err) = validate_ls_colors_env() { - if let LsColorsParseError::UnrecognizedPrefix(prefix) = &err { - show_warning!( - "{}", - translate!( - "ls-warning-unrecognized-ls-colors-prefix", - "prefix" => prefix.quote() - ) - ); - } - show_warning!("{}", translate!("ls-warning-unparsable-ls-colors")); - needs_color = false; - } - } - - let color = if needs_color { - Some(LsColors::from_env().unwrap_or_default()) - } else { - None - }; - - if dired || is_dired_arg_present() { - // --dired implies --format=long - // if we have --dired --hyperlink, we don't show dired but we still want to see the - // long format - format = Format::Long; - } - if dired && options.get_flag(options::ZERO) { - return Err(Box::new(LsError::DiredAndZeroAreIncompatible)); - } - - let dereference = if options.get_flag(options::dereference::ALL) { - Dereference::All - } else if options.get_flag(options::dereference::ARGS) { - Dereference::Args - } else if options.get_flag(options::dereference::DIR_ARGS) { - Dereference::DirArgs - } else if options.get_flag(options::DIRECTORY) - || indicator_style == IndicatorStyle::Classify - || format == Format::Long - { - Dereference::None - } else { - Dereference::DirArgs - }; - - let tab_size = if needs_color { - Some(0) - } else { - options - .get_one::(options::format::TAB_SIZE) - .and_then(|size| size.parse::().ok()) - .or_else(|| std::env::var("TABSIZE").ok().and_then(|s| s.parse().ok())) - } - .unwrap_or(SPACES_IN_TAB); - - Ok(Self { - format, - files, - sort, - recursive: options.get_flag(options::RECURSIVE), - reverse: options.get_flag(options::REVERSE), - dereference, - ignore_patterns, - size_format, - directory: options.get_flag(options::DIRECTORY), - time, - color, - #[cfg(unix)] - inode: options.get_flag(options::INODE), - long, - alloc_size: options.get_flag(options::size::ALLOCATION_SIZE), - file_size_block_size, - block_size, - width, - quoting_style, - locale_quoting, - indicator_style, - time_format_recent, - time_format_older, - context, - #[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))] - selinux_supported: uucore::selinux::is_selinux_enabled(), - #[cfg(all(feature = "smack", target_os = "linux"))] - smack_supported: uucore::smack::is_smack_enabled(), - group_directories_first: options.get_flag(options::GROUP_DIRECTORIES_FIRST), - line_ending: LineEnding::from_zero_flag(options.get_flag(options::ZERO)), - dired, - hyperlink, - tab_size, - }) - } -} - -#[uucore::main] -pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uucore::clap_localization::handle_clap_result_with_exit_code(uu_app(), args, 2)?; - - let config = Config::from(&matches)?; - - let locs = matches - .get_many::(options::PATHS) - .map_or_else(|| vec![Path::new(".")], |v| v.map(Path::new).collect()); - - list(locs, &config) + list(locs, &config) } pub fn uu_app() -> Command { @@ -2097,98 +960,6 @@ impl Colorable for PathData { } } -/// Show the directory name in the case where several arguments are given to ls -/// or the recursive flag is passed. -/// -/// ```no-exec -/// $ ls -R -/// .: <- This is printed by this function -/// dir1 file1 file2 -/// -/// dir1: <- This as well -/// file11 -/// ``` -fn show_dir_name( - path_data: &PathData, - out: &mut BufWriter, - config: &Config, -) -> std::io::Result<()> { - let escaped_name = escape_dir_name_with_locale(path_data.path().as_os_str(), config); - - let name = if config.hyperlink && !config.dired { - create_hyperlink(&escaped_name, path_data) - } else { - escaped_name - }; - - write_os_str(out, &name)?; - write!(out, ":") -} - -fn escape_with_locale(name: &OsStr, config: &Config, fallback: F) -> OsString -where - F: FnOnce(&OsStr, QuotingStyle) -> OsString, -{ - if let Some(locale) = config.locale_quoting { - locale_quote(name, locale) - } else { - fallback(name, config.quoting_style) - } -} - -fn escape_dir_name_with_locale(name: &OsStr, config: &Config) -> OsString { - escape_with_locale(name, config, locale_aware_escape_dir_name) -} - -fn escape_name_with_locale(name: &OsStr, config: &Config) -> OsString { - escape_with_locale(name, config, locale_aware_escape_name) -} - -fn locale_quote(name: &OsStr, style: LocaleQuoting) -> OsString { - let bytes = os_str_as_bytes_lossy(name); - let mut quoted = String::new(); - match style { - LocaleQuoting::Single => quoted.push('\''), - LocaleQuoting::Double => quoted.push('"'), - } - for &byte in bytes.as_ref() { - push_locale_byte(&mut quoted, byte, style); - } - match style { - LocaleQuoting::Single => quoted.push('\''), - LocaleQuoting::Double => quoted.push('"'), - } - OsString::from(quoted) -} - -fn push_locale_byte(buf: &mut String, byte: u8, style: LocaleQuoting) { - match (style, byte) { - (LocaleQuoting::Single, b'\'') => buf.push_str("'\\''"), - (LocaleQuoting::Double, b'"') => buf.push_str("\\\""), - (_, b'\\') => buf.push_str("\\\\"), - _ => push_basic_escape(buf, byte), - } -} - -fn push_basic_escape(buf: &mut String, byte: u8) { - match byte { - b'\x07' => buf.push_str("\\a"), - b'\x08' => buf.push_str("\\b"), - b'\t' => buf.push_str("\\t"), - b'\n' => buf.push_str("\\n"), - b'\x0b' => buf.push_str("\\v"), - b'\x0c' => buf.push_str("\\f"), - b'\r' => buf.push_str("\\r"), - b'\x1b' => buf.push_str("\\e"), - b'"' => buf.push('"'), - b'\'' => buf.push('\''), - b if (0x20..=0x7e).contains(&b) => buf.push(b as char), - _ => { - let _ = write!(buf, "\\{byte:03o}"); - } - } -} - type DirData = (PathBuf, bool); // A struct to encapsulate state that is passed around from `list` functions. @@ -2403,1206 +1174,213 @@ fn sort_entries(entries: &mut [PathData], config: &Config) { } Some(ft) => ft.is_dir(), } - }); - } -} - -fn is_hidden(file_path: &DirEntry) -> bool { - #[cfg(windows)] - { - let metadata = file_path.metadata().unwrap(); - let attr = metadata.file_attributes(); - (attr & 0x2) > 0 - } - #[cfg(not(windows))] - { - file_path.file_name().as_encoded_bytes().starts_with(b".") - } -} - -fn should_display(entry: &DirEntry, config: &Config) -> bool { - // check if hidden - if config.files == Files::Normal && is_hidden(entry) { - return false; - } - - // check if it is among ignore_patterns - let options = MatchOptions { - // setting require_literal_leading_dot to match behavior in GNU ls - require_literal_leading_dot: true, - require_literal_separator: false, - case_sensitive: true, - }; - - let file_name = entry.file_name(); - // If the decoding fails, still match best we can - // FIXME: use OsStrings or Paths once we have a glob crate that supports it: - // https://github.com/rust-lang/glob/issues/23 - // https://github.com/rust-lang/glob/issues/78 - // https://github.com/BurntSushi/ripgrep/issues/1250 - - let file_name = match file_name.to_str() { - Some(s) => Cow::Borrowed(s), - None => file_name.to_string_lossy(), - }; - - !config - .ignore_patterns - .iter() - .any(|p| p.matches_with(&file_name, options)) -} - -fn depth_first_list( - (dir_path, needs_blank_line): DirData, - mut read_dir: ReadDir, - config: &Config, - state: &mut ListState, - dired: &mut DiredOutput, - is_top_level: bool, -) -> UResult<()> { - let path_data = PathData::new(dir_path, None, None, config, false); - - // Print dir heading - name... 'total' comes after error display - if state.initial_locs_len > 1 || config.recursive { - if is_top_level { - if needs_blank_line { - writeln!(state.out)?; - if config.dired { - dired.padding += 1; - } - } - if config.dired { - dired::indent(&mut state.out)?; - } - show_dir_name(&path_data, &mut state.out, config)?; - writeln!(state.out)?; - if config.dired { - let dir_len = path_data.path().as_os_str().len(); - // add the //SUBDIRED// coordinates - dired::calculate_subdired(dired, dir_len); - // Add the padding for the dir name - dired::add_dir_name(dired, dir_len); - } - } else { - writeln!(state.out)?; - if config.dired { - dired.padding += 1; - dired::indent(&mut state.out)?; - let dir_name_size = path_data.path().as_os_str().len(); - dired::calculate_subdired(dired, dir_name_size); - dired::add_dir_name(dired, dir_name_size); - } - show_dir_name(&path_data, &mut state.out, config)?; - writeln!(state.out)?; - } - } - - // Append entries with initial dot files and record their existence - let (ref mut buf, trim) = if config.files == Files::All { - const DOT_DIRECTORIES: usize = 2; - let v = vec![ - PathData::new( - path_data.path().to_path_buf(), - None, - Some(".".into()), - config, - false, - ), - PathData::new( - path_data.path().join(".."), - None, - Some("..".into()), - config, - false, - ), - ]; - (v, DOT_DIRECTORIES) - } else { - (Vec::new(), 0) - }; - - // Convert those entries to the PathData struct - for raw_entry in read_dir.by_ref() { - match raw_entry { - Ok(dir_entry) => { - if should_display(&dir_entry, config) { - buf.push(PathData::new( - dir_entry.path(), - Some(dir_entry), - None, - config, - false, - )); - } - } - Err(err) => { - state.out.flush()?; - show!(LsError::IOError(err)); - } - } - } - // Relinquish unused space since we won't need it anymore. - buf.shrink_to_fit(); - - sort_entries(buf, config); - - if config.format == Format::Long || config.alloc_size { - let total = return_total(buf, config, &mut state.out)?; - write!(state.out, "{}", total.as_str())?; - if config.dired { - dired::add_total(dired, total.len()); - } - } - - display_items(buf, config, state, dired)?; - - if config.recursive { - for e in buf - .iter() - .skip(trim) - .filter(|p| p.file_type().is_some_and(FileType::is_dir)) - .rev() - { - // Try to open only to report any errors in order to match GNU semantics. - if let Err(err) = fs::read_dir(e.path()) { - state.out.flush()?; - show!(LsError::IOErrorContext( - e.path().to_path_buf(), - err, - e.command_line - )); - } else { - let fi = FileInformation::from_path(e.path(), e.must_dereference)?; - if state.listed_ancestors.insert(fi) { - // Push to stack, but with a less aggressive growth curve. - let (cap, len) = (state.stack.capacity(), state.stack.len()); - if cap == len { - state.stack.reserve_exact(len / 4 + 4); - } - state.stack.push((e.path().to_path_buf(), true)); - } else { - state.out.flush()?; - show!(LsError::AlreadyListedError(e.path().to_path_buf())); - } - } - } - } - Ok(()) -} - -fn get_metadata_with_deref_opt(p_buf: &Path, dereference: bool) -> std::io::Result { - if dereference { - p_buf.metadata() - } else { - p_buf.symlink_metadata() - } -} - -fn display_dir_entry_size( - entry: &PathData, - config: &Config, - state: &mut ListState, -) -> (usize, usize, usize, usize, usize, usize) { - // TODO: Cache/memorize the display_* results so we don't have to recalculate them. - if let Some(md) = entry.metadata() { - let (size_len, major_len, minor_len) = match display_len_or_rdev(md, config) { - SizeOrDeviceId::Device(major, minor) => { - (major.len() + minor.len() + 2usize, major.len(), minor.len()) - } - SizeOrDeviceId::Size(size) => (size.len(), 0usize, 0usize), - }; - ( - display_symlink_count(md).len(), - display_uname(md, config, state).len(), - display_group(md, config, state).len(), - size_len, - major_len, - minor_len, - ) - } else { - (0, 0, 0, 0, 0, 0) - } -} - -// A simple, performant, ExtendPad trait to add a string to a Vec, padding with spaces -// on the left or right, without making additional copies, or using formatting functions. -trait ExtendPad { - fn extend_pad_left(&mut self, string: &str, count: usize); - fn extend_pad_right(&mut self, string: &str, count: usize); -} - -impl ExtendPad for Vec { - fn extend_pad_left(&mut self, string: &str, count: usize) { - if string.len() < count { - self.extend(iter::repeat_n(b' ', count - string.len())); - } - self.extend(string.as_bytes()); - } - - fn extend_pad_right(&mut self, string: &str, count: usize) { - self.extend(string.as_bytes()); - if string.len() < count { - self.extend(iter::repeat_n(b' ', count - string.len())); - } - } -} - -// TODO: Consider converting callers to use ExtendPad instead, as it avoids -// additional copies. -fn pad_left(string: &str, count: usize) -> String { - format!("{string:>count$}") -} - -fn return_total( - items: &[PathData], - config: &Config, - out: &mut BufWriter, -) -> UResult { - let mut total_size = 0; - for item in items { - total_size += item - .metadata() - .as_ref() - .map_or(0, |md| get_block_size(md, config)); - } - if config.dired { - dired::indent(out)?; - } - Ok(format!( - "{}{}", - translate!("ls-total", "size" => display_size(total_size, config)), - config.line_ending - )) -} - -fn display_additional_leading_info( - item: &PathData, - padding: &PaddingCollection, - config: &Config, -) -> String { - let mut result = String::new(); - #[cfg(unix)] - { - if config.inode { - let i = if let Some(md) = item.metadata() { - get_inode(md) - } else { - "?".to_owned() - }; - write!(result, "{} ", pad_left(&i, padding.inode)).unwrap(); - } - } - - if config.alloc_size { - let s = if let Some(md) = item.metadata() { - display_size(get_block_size(md, config), config) - } else { - "?".to_owned() - }; - // extra space is insert to align the sizes, as needed for all formats, except for the comma format. - if config.format == Format::Commas { - write!(result, "{s} ").unwrap(); - } else { - write!(result, "{} ", pad_left(&s, padding.block_size)).unwrap(); - } - } - - result -} - -#[allow(clippy::cognitive_complexity)] -fn display_items( - items: &[PathData], - config: &Config, - state: &mut ListState, - dired: &mut DiredOutput, -) -> UResult<()> { - // `-Z`, `--context`: - // Display the SELinux security context or '?' if none is found. When used with the `-l` - // option, print the security context to the left of the size column. - - let quoted = items.iter().any(|item| { - let name = escape_name_with_locale(item.display_name(), config); - os_str_starts_with(&name, b"'") - }); - - if config.format == Format::Long { - let padding_collection = calculate_padding_collection(items, config, state); - - for item in items { - #[cfg(unix)] - let should_display_leading_info = config.inode || config.alloc_size; - #[cfg(not(unix))] - let should_display_leading_info = config.alloc_size; - - if should_display_leading_info { - let more_info = display_additional_leading_info(item, &padding_collection, config); - - write!(state.out, "{more_info}")?; - } - - display_item_long(item, &padding_collection, config, state, dired, quoted)?; - } - } else { - let mut longest_context_len = 1; - let prefix_context = if config.context { - for item in items { - let context_len = item.security_context(config).len(); - longest_context_len = context_len.max(longest_context_len); - } - Some(longest_context_len) - } else { - None - }; - - let padding = calculate_padding_collection(items, config, state); - - // we need to apply normal color to non filename output - if let Some(style_manager) = &mut state.style_manager { - write!(state.out, "{}", style_manager.apply_normal())?; - } - - let mut names_vec = Vec::new(); - - #[cfg(unix)] - let should_display_leading_info = config.inode || config.alloc_size; - #[cfg(not(unix))] - let should_display_leading_info = config.alloc_size; - - for i in items { - let more_info = if should_display_leading_info { - Some(display_additional_leading_info(i, &padding, config)) - } else { - None - }; - // it's okay to set current column to zero which is used to decide - // whether text will wrap or not, because when format is grid or - // column ls will try to place the item name in a new line if it - // wraps. - let cell = display_item_name( - i, - config, - prefix_context, - more_info, - state, - LazyCell::new(Box::new(|| 0)), - ); - - names_vec.push(cell.displayed); - } - - let mut names = names_vec.into_iter(); - - match config.format { - Format::Columns => { - display_grid( - names, - config.width, - Direction::TopToBottom, - &mut state.out, - quoted, - config.tab_size, - )?; - } - Format::Across => { - display_grid( - names, - config.width, - Direction::LeftToRight, - &mut state.out, - quoted, - config.tab_size, - )?; - } - Format::Commas => { - let mut current_col = 0; - if let Some(name) = names.next() { - write_os_str(&mut state.out, &name)?; - current_col = ansi_width(&name.to_string_lossy()) as u16 + 2; - } - for name in names { - let name_width = ansi_width(&name.to_string_lossy()) as u16; - // If the width is 0 we print one single line - if config.width != 0 && current_col + name_width + 1 > config.width { - current_col = name_width + 2; - writeln!(state.out, ",")?; - } else { - current_col += name_width + 2; - write!(state.out, ", ")?; - } - write_os_str(&mut state.out, &name)?; - } - // Current col is never zero again if names have been printed. - // So we print a newline. - if current_col > 0 { - write!(state.out, "{}", config.line_ending)?; - } - } - _ => { - for name in names { - write_os_str(&mut state.out, &name)?; - write!(state.out, "{}", config.line_ending)?; - } - } - } - } - - Ok(()) -} - -#[allow(unused_variables)] -fn get_block_size(md: &Metadata, config: &Config) -> u64 { - /* GNU ls will display sizes in terms of block size - md.len() will differ from this value when the file has some holes - */ - #[cfg(unix)] - { - let raw_blocks = if md.file_type().is_char_device() || md.file_type().is_block_device() { - 0u64 - } else { - md.blocks() * 512 - }; - match config.size_format { - SizeFormat::Binary | SizeFormat::Decimal => raw_blocks, - SizeFormat::Bytes => raw_blocks / config.block_size, - } - } - #[cfg(not(unix))] - { - // no way to get block size for windows, fall-back to file size - md.len() - } -} - -fn display_grid( - names: impl Iterator, - width: u16, - direction: Direction, - out: &mut BufWriter, - quoted: bool, - tab_size: usize, -) -> UResult<()> { - if width == 0 { - // If the width is 0 we print one single line - let mut printed_something = false; - for name in names { - if printed_something { - write!(out, " ")?; - } - printed_something = true; - write_os_str(out, &name)?; - } - if printed_something { - writeln!(out)?; - } - } else { - let names: Vec<_> = if quoted { - // In case some names are quoted, GNU adds a space before each - // entry that does not start with a quote to make it prettier - // on multiline. - // - // Example: - // ``` - // $ ls - // 'a\nb' bar - // foo baz - // ^ ^ - // These spaces is added - // ``` - names - .map(|n| { - if os_str_starts_with(&n, b"'") || os_str_starts_with(&n, b"\"") { - n - } else { - let mut ret: OsString = " ".into(); - ret.push(n); - ret - } - }) - .collect() - } else { - names.collect() - }; - - // FIXME: the Grid crate only supports &str, so can't display raw bytes - let names: Vec<_> = names - .into_iter() - .map(|s| s.to_string_lossy().into_owned()) - .collect(); - - // Since tab_size=0 means no \t, use Spaces separator for optimization. - let filling = match tab_size { - 0 => Filling::Spaces(DEFAULT_SEPARATOR_SIZE), - _ => Filling::Tabs { - spaces: DEFAULT_SEPARATOR_SIZE, - tab_size, - }, - }; - - let grid = Grid::new( - names, - GridOptions { - filling, - direction, - width: width as usize, - }, - ); - write!(out, "{grid}")?; - } - Ok(()) -} - -fn calculate_line_len(output_len: usize, item_len: usize, line_ending: LineEnding) -> usize { - output_len + item_len + line_ending.to_string().len() -} - -fn update_dired_for_item( - dired: &mut DiredOutput, - output_display_len: usize, - displayed_len: usize, - dired_name_len: usize, - line_ending: LineEnding, -) { - let line_len = calculate_line_len(output_display_len, displayed_len, line_ending); - dired::calculate_and_update_positions(dired, output_display_len, dired_name_len, line_len); -} - -/// This writes to the [`BufWriter`] `state.out` a single string of the output of `ls -l`. -/// -/// It writes the following keys, in order: -/// * `inode` ([`get_inode`], config-optional) -/// * `permissions` ([`display_permissions`]) -/// * `symlink_count` ([`display_symlink_count`]) -/// * `owner` ([`display_uname`], config-optional) -/// * `group` ([`display_group`], config-optional) -/// * `author` ([`display_uname`], config-optional) -/// * `size / rdev` ([`display_len_or_rdev`]) -/// * `system_time` ([`display_date`]) -/// * `item_name` ([`display_item_name`]) -/// -/// This function needs to display information in columns: -/// * permissions and `system_time` are already guaranteed to be pre-formatted in fixed length. -/// * `item_name` is the last column and is left-aligned. -/// * Everything else needs to be padded using [`pad_left`]. -/// -/// That's why we have the parameters: -/// ```txt -/// longest_link_count_len: usize, -/// longest_uname_len: usize, -/// longest_group_len: usize, -/// longest_context_len: usize, -/// longest_size_len: usize, -/// ``` -/// that decide the maximum possible character count of each field. -#[allow(clippy::write_literal)] -#[allow(clippy::cognitive_complexity)] -fn display_item_long( - item: &PathData, - padding: &PaddingCollection, - config: &Config, - state: &mut ListState, - dired: &mut DiredOutput, - quoted: bool, -) -> UResult<()> { - let mut output_display: Vec = Vec::with_capacity(128); - - // apply normal color to non filename outputs - if let Some(style_manager) = &mut state.style_manager { - output_display.extend(style_manager.apply_normal().as_bytes()); - } - if config.dired { - output_display.extend(b" "); - } - if let Some(md) = item.metadata() { - #[cfg(any(not(unix), target_os = "android", target_os = "macos"))] - // TODO: See how Mac should work here - let is_acl_set = false; - #[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] - let is_acl_set = has_acl(item.path()); - output_display.extend(display_permissions(md, true).as_bytes()); - if item.security_context(config).len() > 1 { - // GNU `ls` uses a "." character to indicate a file with a security context, - // but not other alternate access method. - output_display.extend(b"."); - } else if is_acl_set { - output_display.extend(b"+"); - } else { - output_display.extend(b" "); - } - - output_display.extend_pad_left(&display_symlink_count(md), padding.link_count); - - if config.long.owner { - output_display.extend(b" "); - output_display.extend_pad_right(display_uname(md, config, state), padding.uname); - } - - if config.long.group { - output_display.extend(b" "); - output_display.extend_pad_right(display_group(md, config, state), padding.group); - } - - if config.context { - output_display.extend(b" "); - output_display.extend_pad_right(item.security_context(config), padding.context); - } - - // Author is only different from owner on GNU/Hurd, so we reuse - // the owner, since GNU/Hurd is not currently supported by Rust. - if config.long.author { - output_display.extend(b" "); - output_display.extend_pad_right(display_uname(md, config, state), padding.uname); - } - - match display_len_or_rdev(md, config) { - SizeOrDeviceId::Size(size) => { - output_display.extend(b" "); - output_display.extend_pad_left(&size, padding.size); - } - SizeOrDeviceId::Device(major, minor) => { - output_display.extend(b" "); - output_display.extend_pad_left( - &major, - #[cfg(not(unix))] - 0usize, - #[cfg(unix)] - padding.major.max( - padding - .size - .saturating_sub(padding.minor.saturating_add(2usize)), - ), - ); - output_display.extend(b", "); - output_display.extend_pad_left( - &minor, - #[cfg(not(unix))] - 0usize, - #[cfg(unix)] - padding.minor, - ); - } - } - - output_display.extend(b" "); - display_date(md, config, state, &mut output_display)?; - output_display.extend(b" "); - - let item_display = display_item_name( - item, - config, - None, - None, - state, - LazyCell::new(Box::new(|| { - ansi_width(&String::from_utf8_lossy(&output_display)) - })), - ); - - let needs_space = quoted && !os_str_starts_with(&item_display.displayed, b"'"); - - if config.dired { - let mut dired_name_len = item_display.dired_name_len; - if needs_space { - dired_name_len += 1; - } - let displayed_len = item_display.displayed.len() + usize::from(needs_space); - update_dired_for_item( - dired, - output_display.len(), - displayed_len, - dired_name_len, - config.line_ending, - ); - } - - let item_name = item_display.displayed; - let displayed_item = if needs_space { - let mut ret: OsString = " ".into(); - ret.push(&item_name); - ret - } else { - item_name - }; - - write_os_str(&mut output_display, &displayed_item)?; - output_display.extend(config.line_ending.to_string().as_bytes()); - } else { - #[cfg(unix)] - let leading_char = { - if let Some(ft) = item.file_type() { - if ft.is_char_device() { - "c" - } else if ft.is_block_device() { - "b" - } else if ft.is_symlink() { - "l" - } else if ft.is_dir() { - "d" - } else { - "-" - } - } else if item.is_dangling_link() { - "l" - } else { - "-" - } - }; - #[cfg(not(unix))] - let leading_char = { - if let Some(ft) = item.file_type() { - if ft.is_symlink() { - "l" - } else if ft.is_dir() { - "d" - } else { - "-" - } - } else if item.is_dangling_link() { - "l" - } else { - "-" - } - }; - - output_display.extend(leading_char.as_bytes()); - output_display.extend(b"?????????"); - if item.security_context(config).len() > 1 { - // GNU `ls` uses a "." character to indicate a file with a security context, - // but not other alternate access method. - output_display.extend(b"."); - } - output_display.extend(b" "); - output_display.extend_pad_left("?", padding.link_count); - - if config.long.owner { - output_display.extend(b" "); - output_display.extend_pad_right("?", padding.uname); - } - - if config.long.group { - output_display.extend(b" "); - output_display.extend_pad_right("?", padding.group); - } - - if config.context { - output_display.extend(b" "); - output_display.extend_pad_right(item.security_context(config), padding.context); - } - - // Author is only different from owner on GNU/Hurd, so we reuse - // the owner, since GNU/Hurd is not currently supported by Rust. - if config.long.author { - output_display.extend(b" "); - output_display.extend_pad_right("?", padding.uname); - } - - let displayed_item = display_item_name( - item, - config, - None, - None, - state, - LazyCell::new(Box::new(|| { - ansi_width(&String::from_utf8_lossy(&output_display)) - })), - ); - let date_len = 12; - - output_display.extend(b" "); - output_display.extend_pad_left("?", padding.size); - output_display.extend(b" "); - output_display.extend_pad_left("?", date_len); - output_display.extend(b" "); - - if config.dired { - update_dired_for_item( - dired, - output_display.len(), - displayed_item.displayed.len(), - displayed_item.dired_name_len, - config.line_ending, - ); - } - let displayed_item = displayed_item.displayed; - write_os_str(&mut output_display, &displayed_item)?; - output_display.extend(config.line_ending.to_string().as_bytes()); - } - state.out.write_all(&output_display)?; - - Ok(()) -} - -#[cfg(unix)] -fn get_inode(metadata: &Metadata) -> String { - format!("{}", metadata.ino()) -} - -// Currently getpwuid is `linux` target only. If it's broken state.out into -// a posix-compliant attribute this can be updated... -#[cfg(unix)] -fn display_uname<'a>(metadata: &Metadata, config: &Config, state: &'a mut ListState) -> &'a String { - let uid = metadata.uid(); - - state.uid_cache.entry(uid).or_insert_with(|| { - if config.long.numeric_uid_gid { - uid.to_string() - } else { - entries::uid2usr(uid).unwrap_or_else(|_| uid.to_string()) - } - }) -} - -#[cfg(unix)] -fn display_group<'a>(metadata: &Metadata, config: &Config, state: &'a mut ListState) -> &'a String { - let gid = metadata.gid(); - state.gid_cache.entry(gid).or_insert_with(|| { - if config.long.numeric_uid_gid { - gid.to_string() - } else { - entries::gid2grp(gid).unwrap_or_else(|_| gid.to_string()) - } - }) -} - -#[cfg(not(unix))] -fn display_uname(_metadata: &Metadata, _config: &Config, _state: &mut ListState) -> &'static str { - "somebody" -} - -#[cfg(not(unix))] -fn display_group(_metadata: &Metadata, _config: &Config, _state: &mut ListState) -> &'static str { - "somegroup" -} - -fn display_date( - metadata: &Metadata, - config: &Config, - state: &mut ListState, - out: &mut Vec, -) -> UResult<()> { - let Some(time) = metadata_get_time(metadata, config.time) else { - out.extend(b"???"); - return Ok(()); - }; - - // Use "recent" format if the given date is considered recent (i.e., in the last 6 months), - // or if no "older" format is available. - let fmt = match &config.time_format_older { - Some(time_format_older) if !state.recent_time_range.contains(&time) => time_format_older, - _ => &config.time_format_recent, - }; - - format_system_time(out, time, fmt, FormatSystemTimeFallback::Integer) -} - -#[allow(dead_code)] -enum SizeOrDeviceId { - Size(String), - Device(String, String), -} - -fn display_len_or_rdev(metadata: &Metadata, config: &Config) -> SizeOrDeviceId { - #[cfg(any( - target_os = "linux", - target_os = "macos", - target_os = "android", - target_os = "ios", - target_os = "freebsd", - target_os = "dragonfly", - target_os = "netbsd", - target_os = "openbsd", - target_os = "illumos", - target_os = "solaris" - ))] - { - let ft = metadata.file_type(); - if ft.is_char_device() || ft.is_block_device() { - // A type cast is needed here as the `dev_t` type varies across OSes. - let dev = metadata.rdev() as dev_t; - let major = major(dev); - let minor = minor(dev); - return SizeOrDeviceId::Device(major.to_string(), minor.to_string()); - } - } - let len_adjusted = { - let d = metadata.len() / config.file_size_block_size; - let r = metadata.len() % config.file_size_block_size; - if r == 0 { d } else { d + 1 } - }; - SizeOrDeviceId::Size(display_size(len_adjusted, config)) -} - -fn display_size(size: u64, config: &Config) -> String { - human_readable(size, config.size_format) -} - -#[cfg(unix)] -fn file_is_executable(md: &Metadata) -> bool { - // Mode always returns u32, but the flags might not be, based on the platform - // e.g. linux has u32, mac has u16. - // S_IXUSR -> user has execute permission - // S_IXGRP -> group has execute permission - // S_IXOTH -> other users have execute permission - #[allow(clippy::unnecessary_cast)] - return md.mode() & ((S_IXUSR | S_IXGRP | S_IXOTH) as u32) != 0; -} - -fn classify_file(path: &PathData) -> Option { - let file_type = path.file_type()?; - - if file_type.is_dir() { - Some('/') - } else if file_type.is_symlink() { - Some('@') - } else { - #[cfg(unix)] - { - if file_type.is_socket() { - Some('=') - } else if file_type.is_fifo() { - Some('|') - // Safe unwrapping if the file was removed between listing and display - // See https://github.com/uutils/coreutils/issues/5371 - } else if path.is_executable_file() { - Some('*') - } else { - None - } - } - #[cfg(not(unix))] - None + }); } } -/// Takes a [`PathData`] struct and returns a cell with a name ready for displaying. -/// -/// This function relies on the following parameters in the provided `&Config`: -/// * `config.quoting_style` to decide how we will escape `name` using [`locale_aware_escape_name`]. -/// * `config.inode` decides whether to display inode numbers beside names using [`get_inode`]. -/// * `config.color` decides whether it's going to color `name` using [`color_name`]. -/// * `config.indicator_style` to append specific characters to `name` using [`classify_file`]. -/// * `config.format` to display symlink targets if `Format::Long`. This function is also -/// responsible for coloring symlink target names if `config.color` is specified. -/// * `config.context` to prepend security context to `name` if compiled with `feat_selinux`. -/// * `config.hyperlink` decides whether to hyperlink the item -/// -/// Note that non-unicode sequences in symlink targets are dealt with using -/// [`std::path::Path::to_string_lossy`]. -#[allow(clippy::cognitive_complexity)] -fn display_item_name( - path: &PathData, +fn depth_first_list( + (dir_path, needs_blank_line): DirData, + mut read_dir: ReadDir, config: &Config, - prefix_context: Option, - more_info: Option, state: &mut ListState, - current_column: LazyCell usize + '_>>, -) -> DisplayItemName { - // This is our return value. We start by `&path.display_name` and modify it along the way. - let mut name = escape_name_with_locale(path.display_name(), config); - - let is_wrap = - |namelen: usize| config.width != 0 && *current_column + namelen > config.width.into(); - - if config.hyperlink { - name = create_hyperlink(&name, path); - } - - if let Some(style_manager) = &mut state.style_manager { - let len = name.len(); - name = color_name(name, path, style_manager, None, is_wrap(len)); - } - - if config.format != Format::Long { - if let Some(info) = more_info { - let old_name = name; - name = info.into(); - name.push(&old_name); - } - } - - if config.indicator_style != IndicatorStyle::None { - let sym = classify_file(path); + dired: &mut DiredOutput, + is_top_level: bool, +) -> UResult<()> { + let path_data = PathData::new(dir_path, None, None, config, false); - let char_opt = match config.indicator_style { - IndicatorStyle::Classify => sym, - IndicatorStyle::FileType => { - // Don't append an asterisk. - match sym { - Some('*') => None, - _ => sym, + // Print dir heading - name... 'total' comes after error display + if state.initial_locs_len > 1 || config.recursive { + if is_top_level { + if needs_blank_line { + writeln!(state.out)?; + if config.dired { + dired.padding += 1; } } - IndicatorStyle::Slash => { - // Append only a slash. - match sym { - Some('/') => Some('/'), - _ => None, - } + if config.dired { + dired::indent(&mut state.out)?; } - IndicatorStyle::None => None, - }; - - if let Some(c) = char_opt { - name.push(OsStr::new(&c.to_string())); + show_dir_name(&path_data, &mut state.out, config)?; + writeln!(state.out)?; + if config.dired { + let dir_len = path_data.path().as_os_str().len(); + // add the //SUBDIRED// coordinates + dired::calculate_subdired(dired, dir_len); + // Add the padding for the dir name + dired::add_dir_name(dired, dir_len); + } + } else { + writeln!(state.out)?; + if config.dired { + dired.padding += 1; + dired::indent(&mut state.out)?; + let dir_name_size = path_data.path().as_os_str().len(); + dired::calculate_subdired(dired, dir_name_size); + dired::add_dir_name(dired, dir_name_size); + } + show_dir_name(&path_data, &mut state.out, config)?; + writeln!(state.out)?; } } - let dired_name_len = if config.dired { name.len() } else { 0 }; - - if config.format == Format::Long - && path.file_type().is_some_and(FileType::is_symlink) - && !path.must_dereference - { - match path.path().read_link() { - Ok(target_path) => { - name.push(" -> "); - - // We might as well color the symlink output after the arrow. - // This makes extra system calls, but provides important information that - // people run `ls -l --color` are very interested in. - if let Some(style_manager) = &mut state.style_manager { - let escaped_target = escape_name_with_locale(target_path.as_os_str(), config); - // We get the absolute path to be able to construct PathData with valid Metadata. - // This is because relative symlinks will fail to get_metadata. - let mut absolute_target = target_path.clone(); - if target_path.is_relative() { - if let Some(parent) = path.path().parent() { - absolute_target = parent.join(absolute_target); - } - } + // Append entries with initial dot files and record their existence + let (ref mut buf, trim) = if config.files == Files::All { + const DOT_DIRECTORIES: usize = 2; + let v = vec![ + PathData::new( + path_data.path().to_path_buf(), + None, + Some(".".into()), + config, + false, + ), + PathData::new( + path_data.path().join(".."), + None, + Some("..".into()), + config, + false, + ), + ]; + (v, DOT_DIRECTORIES) + } else { + (Vec::new(), 0) + }; - match fs::canonicalize(&absolute_target) { - Ok(resolved_target) => { - let target_data = PathData::new( - resolved_target, - None, - target_path.file_name().map(OsStr::to_os_string), - config, - false, - ); - - // Check if the target actually needs coloring - let md_option: Option = target_data - .metadata() - .cloned() - .or_else(|| target_data.p_buf.symlink_metadata().ok()); - let style = style_manager.colors.style_for_path_with_metadata( - &target_data.p_buf, - md_option.as_ref(), - ); - - if style.is_some() { - // Only apply coloring if there's actually a style - name.push(color_name( - escaped_target, - &target_data, - style_manager, - None, - is_wrap(name.len()), - )); - } else { - // For regular files with no coloring, just use plain text - name.push(escaped_target); - } - } - Err(_) => { - name.push( - style_manager.apply_missing_target_style( - escaped_target, - is_wrap(name.len()), - ), - ); - } - } - } else { - // If no coloring is required, we just use target as is. - // Apply the right quoting - name.push(escape_name_with_locale(target_path.as_os_str(), config)); + // Convert those entries to the PathData struct + for raw_entry in read_dir.by_ref() { + match raw_entry { + Ok(dir_entry) => { + if should_display(&dir_entry, config) { + buf.push(PathData::new( + dir_entry.path(), + Some(dir_entry), + None, + config, + false, + )); } } Err(err) => { - show!(LsError::IOErrorContext( - path.path().to_path_buf(), - err, - false - )); + state.out.flush()?; + show!(LsError::IOError(err)); } } } + // Relinquish unused space since we won't need it anymore. + buf.shrink_to_fit(); - // Prepend the security context to the `name` and adjust `width` in order - // to get correct alignment from later calls to`display_grid()`. - if config.context { - if let Some(pad_count) = prefix_context { - let security_context = if matches!(config.format, Format::Commas) { - path.security_context(config).to_string() - } else { - pad_left(path.security_context(config), pad_count) - }; + sort_entries(buf, config); - let old_name = name; - name = format!("{security_context} ").into(); - name.push(old_name); + if config.format == Format::Long || config.alloc_size { + let total = return_total(buf, config, &mut state.out)?; + write!(state.out, "{}", total.as_str())?; + if config.dired { + dired::add_total(dired, total.len()); } } - DisplayItemName { - displayed: name, - dired_name_len, - } -} - -fn create_hyperlink(name: &OsStr, path: &PathData) -> OsString { - let hostname = hostname::get().unwrap_or_else(|_| OsString::from("")); - let hostname = hostname.to_string_lossy(); - - let absolute_path = fs::canonicalize(path.path()).unwrap_or_default(); - - // Get bytes for URL encoding in a cross-platform way - let absolute_path_bytes = os_str_as_bytes_lossy(absolute_path.as_os_str()); - - // a set of safe ASCII bytes that don't need encoding - #[cfg(not(target_os = "windows"))] - let unencoded_bytes = b"_-.~/"; - #[cfg(target_os = "windows")] - let unencoded_bytes = b"_-.~/\\:"; + display_items(buf, config, state, dired)?; - // Encode at byte level to properly handle UTF-8 sequences and preserve invalid UTF-8 - let full_encoded_path: String = absolute_path_bytes - .iter() - .map(|&b: &u8| { - if b.is_ascii_alphanumeric() || unencoded_bytes.contains(&b) { - (b as char).to_string() + if config.recursive { + for e in buf + .iter() + .skip(trim) + .filter(|p| p.file_type().is_some_and(FileType::is_dir)) + .rev() + { + // Try to open only to report any errors in order to match GNU semantics. + if let Err(err) = fs::read_dir(e.path()) { + state.out.flush()?; + show!(LsError::IOErrorContext( + e.path().to_path_buf(), + err, + e.command_line + )); } else { - format!("%{b:02x}") + let fi = FileInformation::from_path(e.path(), e.must_dereference)?; + if state.listed_ancestors.insert(fi) { + // Push to stack, but with a less aggressive growth curve. + let (cap, len) = (state.stack.capacity(), state.stack.len()); + if cap == len { + state.stack.reserve_exact(len / 4 + 4); + } + state.stack.push((e.path().to_path_buf(), true)); + } else { + state.out.flush()?; + show!(LsError::AlreadyListedError(e.path().to_path_buf())); + } } - }) - .collect(); - - // OSC 8 hyperlink format: \x1b]8;;URL\x1b\\TEXT\x1b]8;;\x1b\\ - // \x1b = ESC, \x1b\\ = ESC backslash - let mut ret: OsString = format!("\x1b]8;;file://{hostname}{full_encoded_path}\x1b\\").into(); - ret.push(name); - ret.push("\x1b]8;;\x1b\\"); + } + } + Ok(()) +} - ret +fn get_metadata_with_deref_opt(p_buf: &Path, dereference: bool) -> std::io::Result { + if dereference { + p_buf.metadata() + } else { + p_buf.symlink_metadata() + } } -#[cfg(not(unix))] -fn display_symlink_count(_metadata: &Metadata) -> String { - // Currently not sure of how to get this on Windows, so I'm punting. - // Git Bash looks like it may do the same thing. - String::from("1") +fn return_total( + items: &[PathData], + config: &Config, + out: &mut BufWriter, +) -> UResult { + let mut total_size = 0; + for item in items { + total_size += item + .metadata() + .as_ref() + .map_or(0, |md| get_block_size(md, config)); + } + if config.dired { + dired::indent(out)?; + } + Ok(format!( + "{}{}", + translate!("ls-total", "size" => display_size(total_size, config)), + config.line_ending + )) } -#[cfg(unix)] -fn display_symlink_count(metadata: &Metadata) -> String { - metadata.nlink().to_string() +#[allow(unused_variables)] +fn get_block_size(md: &Metadata, config: &Config) -> u64 { + /* GNU ls will display sizes in terms of block size + md.len() will differ from this value when the file has some holes + */ + #[cfg(unix)] + { + use uucore::format::human::SizeFormat; + + let raw_blocks = if md.file_type().is_char_device() || md.file_type().is_block_device() { + 0u64 + } else { + md.blocks() * 512 + }; + match config.size_format { + SizeFormat::Binary | SizeFormat::Decimal => raw_blocks, + SizeFormat::Bytes => raw_blocks / config.block_size, + } + } + #[cfg(not(unix))] + { + // no way to get block size for windows, fall-back to file size + md.len() + } } #[cfg(unix)] -fn display_inode(metadata: &Metadata) -> String { - get_inode(metadata) +fn file_is_executable(md: &Metadata) -> bool { + // Mode always returns u32, but the flags might not be, based on the platform + // e.g. linux has u32, mac has u16. + // S_IXUSR -> user has execute permission + // S_IXGRP -> group has execute permission + // S_IXOTH -> other users have execute permission + #[allow(clippy::unnecessary_cast)] + return md.mode() & ((S_IXUSR | S_IXGRP | S_IXOTH) as u32) != 0; } /// This returns the `SELinux` security context as UTF8 `String`. @@ -3631,6 +1409,8 @@ fn get_security_context<'a>( #[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))] if config.selinux_supported { + use uucore::show_warning; + match selinux::SecurityContext::of_path(path, must_dereference, false) { Err(_r) => { // TODO: show the actual reason why it failed @@ -3683,125 +1463,3 @@ fn get_security_context<'a>( Cow::Borrowed(SUBSTITUTE_STRING) } - -#[cfg(unix)] -fn calculate_padding_collection( - items: &[PathData], - config: &Config, - state: &mut ListState, -) -> PaddingCollection { - let mut padding_collections = PaddingCollection { - inode: 1, - link_count: 1, - uname: 1, - group: 1, - context: 1, - size: 1, - major: 1, - minor: 1, - block_size: 1, - }; - - for item in items { - #[cfg(unix)] - if config.inode { - let inode_len = if let Some(md) = item.metadata() { - display_inode(md).len() - } else { - continue; - }; - padding_collections.inode = inode_len.max(padding_collections.inode); - } - - if config.alloc_size { - if let Some(md) = item.metadata() { - let block_size_len = display_size(get_block_size(md, config), config).len(); - padding_collections.block_size = block_size_len.max(padding_collections.block_size); - } - } - - if config.format == Format::Long { - let context_len = item.security_context(config).len(); - let (link_count_len, uname_len, group_len, size_len, major_len, minor_len) = - display_dir_entry_size(item, config, state); - padding_collections.link_count = link_count_len.max(padding_collections.link_count); - padding_collections.uname = uname_len.max(padding_collections.uname); - padding_collections.group = group_len.max(padding_collections.group); - if config.context { - padding_collections.context = context_len.max(padding_collections.context); - } - - // correctly align columns when some files have capabilities/ACLs and others do not - { - #[cfg(any(not(unix), target_os = "android", target_os = "macos"))] - // TODO: See how Mac should work here - let is_acl_set = false; - #[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] - let is_acl_set = has_acl(item.display_name()); - if context_len > 1 || is_acl_set { - padding_collections.link_count += 1; - } - } - - if items.len() == 1usize { - padding_collections.size = 0usize; - padding_collections.major = 0usize; - padding_collections.minor = 0usize; - } else { - padding_collections.major = major_len.max(padding_collections.major); - padding_collections.minor = minor_len.max(padding_collections.minor); - padding_collections.size = size_len - .max(padding_collections.size) - .max(padding_collections.major); - } - } - } - - padding_collections -} - -#[cfg(not(unix))] -fn calculate_padding_collection( - items: &[PathData], - config: &Config, - state: &mut ListState, -) -> PaddingCollection { - let mut padding_collections = PaddingCollection { - link_count: 1, - uname: 1, - group: 1, - context: 1, - size: 1, - block_size: 1, - }; - - for item in items { - if config.alloc_size { - if let Some(md) = item.metadata() { - let block_size_len = display_size(get_block_size(md, config), config).len(); - padding_collections.block_size = block_size_len.max(padding_collections.block_size); - } - } - - let context_len = item.security_context(config).len(); - let (link_count_len, uname_len, group_len, size_len, _major_len, _minor_len) = - display_dir_entry_size(item, config, state); - padding_collections.link_count = link_count_len.max(padding_collections.link_count); - padding_collections.uname = uname_len.max(padding_collections.uname); - padding_collections.group = group_len.max(padding_collections.group); - if config.context { - padding_collections.context = context_len.max(padding_collections.context); - } - padding_collections.size = size_len.max(padding_collections.size); - } - - padding_collections -} - -fn os_str_starts_with(haystack: &OsStr, needle: &[u8]) -> bool { - os_str_as_bytes_lossy(haystack).starts_with(needle) -} - -fn write_os_str(writer: &mut W, string: &OsStr) -> std::io::Result<()> { - writer.write_all(&os_str_as_bytes_lossy(string)) -} From 31ee0cf0e29c5ecf9e32264ce500cd81a7ee7972 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Fri, 20 Mar 2026 19:53:57 +0900 Subject: [PATCH 013/264] run-gnu-tests-smack-ci.sh: Use alpine busybox --- util/run-gnu-tests-smack-ci.sh | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/util/run-gnu-tests-smack-ci.sh b/util/run-gnu-tests-smack-ci.sh index e37e29ca3fa..867a15dad4a 100755 --- a/util/run-gnu-tests-smack-ci.sh +++ b/util/run-gnu-tests-smack-ci.sh @@ -17,15 +17,14 @@ mkdir -p "$QEMU_DIR"/{rootfs/{bin,lib64,proc,sys,dev,tmp,etc,gnu},kernel} # Copy Ubuntu kernel (runner's kernel does not work) sudo apt-get update || : -sudo apt-get install -y linux-image-generic +sudo apt-get install -y --no-install-recommends linux-image-generic sudo install -Dvm644 "$(ls -1 /boot/vmlinuz-*-generic | head -n 1)" "$QEMU_DIR/kernel/vmlinuz" # Setup busybox -BUSYBOX=/tmp/busybox -[ -f "$BUSYBOX" ] || curl -sL -o "$BUSYBOX" https://busybox.net/downloads/binaries/1.35.0-x86_64-linux-musl/busybox -chmod +x "$BUSYBOX" -cp "$BUSYBOX" "$QEMU_DIR/rootfs/bin/" -(cd "$QEMU_DIR/rootfs/bin" && "$BUSYBOX" --list | xargs -I{} ln -sf busybox {} 2>/dev/null) +curl -L -o b.tar.gz https://dl-cdn.alpinelinux.org/alpine/latest-stable/main/x86_64/busybox-static-1.37.0-r30.apk +tar -xf b.tar.gz +install -Dvm755 bin/busybox.static "$QEMU_DIR/rootfs/bin/busybox" +(cd "$QEMU_DIR/rootfs/bin" && ./busybox --list | xargs -I{} ln -sf busybox {} 2>/dev/null) # Copy required libraries for lib in ld-linux-x86-64.so.2 libc.so.6 libm.so.6 libgcc_s.so.1 libpthread.so.0 libdl.so.2 librt.so.1; do @@ -104,7 +103,7 @@ for TEST_PATH in $QEMU_TESTS; do # Hardlink utilities for SMACK/ROOTFS tests for U in $("$REPO_DIR/target/${PROFILE}/coreutils" --list); do - ln -vf "$REPO_DIR/target/${PROFILE}/coreutils" "$WORK/bin/$U" + ln -f "$REPO_DIR/target/${PROFILE}/coreutils" "$WORK/bin/$U" done # Set test script path and user From 1a5fbf9d2276004b92b4ebac7ac3b5ef77a053c6 Mon Sep 17 00:00:00 2001 From: Dorian Peron Date: Sun, 8 Feb 2026 13:23:52 +0100 Subject: [PATCH 014/264] cksum: Add support for blake3 --- src/uu/b2sum/src/b2sum.rs | 10 +--- src/uu/cksum/src/cksum.rs | 8 ++- src/uucore/src/lib/features/checksum/mod.rs | 56 +++++++++++++------ .../src/lib/features/checksum/validate.rs | 39 ++++++------- src/uucore/src/lib/features/sum.rs | 37 +++++++++--- 5 files changed, 96 insertions(+), 54 deletions(-) diff --git a/src/uu/b2sum/src/b2sum.rs b/src/uu/b2sum/src/b2sum.rs index 6df276f2393..ddd5fe3e9e6 100644 --- a/src/uu/b2sum/src/b2sum.rs +++ b/src/uu/b2sum/src/b2sum.rs @@ -9,18 +9,14 @@ use clap::Command; use uu_checksum_common::{standalone_checksum_app_with_length, standalone_with_length_main}; -use uucore::checksum::{AlgoKind, calculate_blake2b_length_str}; +use uucore::checksum::{AlgoKind, calculate_blake_length_str}; use uucore::error::UResult; use uucore::translate; #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - standalone_with_length_main( - AlgoKind::Blake2b, - uu_app(), - args, - calculate_blake2b_length_str, - ) + let calculate_blake2b_length = |s: &str| calculate_blake_length_str(AlgoKind::Blake2b, s); + standalone_with_length_main(AlgoKind::Blake2b, uu_app(), args, calculate_blake2b_length) } #[inline] diff --git a/src/uu/cksum/src/cksum.rs b/src/uu/cksum/src/cksum.rs index e484c8323ad..19898def4f9 100644 --- a/src/uu/cksum/src/cksum.rs +++ b/src/uu/cksum/src/cksum.rs @@ -12,7 +12,7 @@ use uu_checksum_common::{ChecksumCommand, checksum_main, default_checksum_app, o use uucore::checksum::compute::OutputFormat; use uucore::checksum::{ - AlgoKind, ChecksumError, calculate_blake2b_length_str, sanitize_sha2_sha3_length_str, + AlgoKind, ChecksumError, calculate_blake_length_str, sanitize_sha2_sha3_length_str, }; use uucore::error::UResult; use uucore::hardware::{HasHardwareFeatures as _, SimdPolicy}; @@ -67,8 +67,10 @@ fn maybe_sanitize_length( Err(_) => Err(ChecksumError::InvalidLength(len.into()).into()), }, - // For BLAKE2b, if a length is provided, validate it. - (Some(AlgoKind::Blake2b), Some(len)) => calculate_blake2b_length_str(len), + // For BLAKE, if a length is provided, validate it. + (Some(algo @ (AlgoKind::Blake2b | AlgoKind::Blake3)), Some(len)) => { + calculate_blake_length_str(algo, len) + } // For any other provided algorithm, check if length is 0. // Otherwise, this is an error. diff --git a/src/uucore/src/lib/features/checksum/mod.rs b/src/uucore/src/lib/features/checksum/mod.rs index 1f77b7e635f..9433bbf3682 100644 --- a/src/uucore/src/lib/features/checksum/mod.rs +++ b/src/uucore/src/lib/features/checksum/mod.rs @@ -115,6 +115,8 @@ impl AlgoKind { ALGORITHM_OPTIONS_SHA384 => Sha384, ALGORITHM_OPTIONS_SHA512 => Sha512, + // Extensions not in GNU as of version 9.10 + ALGORITHM_OPTIONS_BLAKE3 => Blake3, ALGORITHM_OPTIONS_SHAKE128 => Shake128, ALGORITHM_OPTIONS_SHAKE256 => Shake256, _ => return Err(ChecksumError::UnknownAlgorithm(algo.as_ref().to_string()).into()), @@ -245,11 +247,11 @@ pub enum SizedAlgoKind { Md5, Sm3, Sha1, - Blake3, Sha2(ShaLength), Sha3(ShaLength), - // Note: we store Blake2b's length as BYTES. + // Note: we store Blake*'s length as BYTES. Blake2b(Option), + Blake3(Option), // Shake* length are stored in bits. Shake128(Option), Shake256(Option), @@ -267,7 +269,6 @@ impl SizedAlgoKind { | ak::Md5 | ak::Sm3 | ak::Sha1 - | ak::Blake3 | ak::Sha224 | ak::Sha256 | ak::Sha384 @@ -282,8 +283,8 @@ impl SizedAlgoKind { (ak::Md5, _) => Ok(Self::Md5), (ak::Sm3, _) => Ok(Self::Sm3), (ak::Sha1, _) => Ok(Self::Sha1), - (ak::Blake3, _) => Ok(Self::Blake3), + (ak::Blake3, l) => Ok(Self::Blake3(l)), (ak::Shake128, l) => Ok(Self::Shake128(l)), (ak::Shake256, l) => Ok(Self::Shake256(l)), (ak::Sha2, Some(l)) => Ok(Self::Sha2(ShaLength::try_from(l)?)), @@ -293,7 +294,8 @@ impl SizedAlgoKind { } // [`calculate_blake2b_length`] expects a length in bits but we // have a length in bytes. - (ak::Blake2b, Some(l)) => Ok(Self::Blake2b(calculate_blake2b_length_str( + (algo @ ak::Blake2b, Some(l)) => Ok(Self::Blake2b(calculate_blake_length_str( + algo, &(8 * l).to_string(), )?)), (ak::Blake2b, None) => Ok(Self::Blake2b(None)), @@ -310,11 +312,16 @@ impl SizedAlgoKind { Self::Md5 => "MD5".into(), Self::Sm3 => "SM3".into(), Self::Sha1 => "SHA1".into(), - Self::Blake3 => "BLAKE3".into(), Self::Sha2(len) => format!("SHA{}", len.as_usize()), Self::Sha3(len) => format!("SHA3-{}", len.as_usize()), Self::Blake2b(Some(byte_len)) => format!("BLAKE2b-{}", byte_len * 8), Self::Blake2b(None) => "BLAKE2b".into(), + Self::Blake3(byte_len) => { + format!( + "BLAKE3-{}", + byte_len.unwrap_or(Blake3::DEFAULT_BYTE_SIZE) * 8 + ) + } Self::Shake128(opt_bit_len) => format!( "SHAKE128-{}", opt_bit_len.unwrap_or(Shake128::DEFAULT_BIT_SIZE) @@ -339,7 +346,6 @@ impl SizedAlgoKind { Self::Md5 => Box::new(Md5::default()), Self::Sm3 => Box::new(Sm3::default()), Self::Sha1 => Box::new(Sha1::default()), - Self::Blake3 => Box::new(Blake3::default()), Self::Sha2(Len224) => Box::new(Sha224::default()), Self::Sha2(Len256) => Box::new(Sha256::default()), Self::Sha2(Len384) => Box::new(Sha384::default()), @@ -351,6 +357,9 @@ impl SizedAlgoKind { Self::Blake2b(len_opt) => { Box::new(len_opt.map(Blake2b::with_output_bytes).unwrap_or_default()) } + Self::Blake3(len_opt) => { + Box::new(len_opt.map(Blake3::with_output_bytes).unwrap_or_default()) + } Self::Shake128(len_opt) => { Box::new(len_opt.map(Shake128::with_output_bits).unwrap_or_default()) } @@ -369,7 +378,7 @@ impl SizedAlgoKind { Self::Md5 => 128, Self::Sm3 => 512, Self::Sha1 => 160, - Self::Blake3 => 256, + Self::Blake3(len) => len.unwrap_or(Blake3::DEFAULT_BYTE_SIZE) * 8, Self::Sha2(len) => len.as_usize(), Self::Sha3(len) => len.as_usize(), Self::Blake2b(len) => len.unwrap_or(Blake2b::DEFAULT_BYTE_SIZE * 8), @@ -486,20 +495,22 @@ pub fn digest_reader( Ok((digest.result(), output_size)) } -/// Calculates the length of the digest. -pub fn calculate_blake2b_length_str(bit_length: &str) -> UResult> { +/// Calculates the BYTE length of the digest. +pub fn calculate_blake_length_str(algo: AlgoKind, bit_length: &str) -> UResult> { + debug_assert!(matches!(algo, AlgoKind::Blake2b | AlgoKind::Blake3)); + // Blake2b's length is parsed in an u64. match bit_length.parse::() { Ok(0) => Ok(None), // Error cases - Ok(n) if n > 512 => { + Ok(n) if n > 512 && algo == AlgoKind::Blake2b => { show_error!("{}", ChecksumError::InvalidLength(bit_length.into())); - Err(ChecksumError::LengthTooBigForBlake("BLAKE2b".into()).into()) + Err(ChecksumError::LengthTooBigForBlake(algo.to_uppercase().into()).into()) } Err(e) if *e.kind() == IntErrorKind::PosOverflow => { show_error!("{}", ChecksumError::InvalidLength(bit_length.into())); - Err(ChecksumError::LengthTooBigForBlake("BLAKE2b".into()).into()) + Err(ChecksumError::LengthTooBigForBlake(algo.to_uppercase().into()).into()) } Err(_) => Err(ChecksumError::InvalidLength(bit_length.into()).into()), @@ -632,10 +643,19 @@ mod tests { #[test] fn test_calculate_blake2b_length() { - assert_eq!(calculate_blake2b_length_str("0").unwrap(), None); - assert!(calculate_blake2b_length_str("10").is_err()); - assert!(calculate_blake2b_length_str("520").is_err()); - assert_eq!(calculate_blake2b_length_str("512").unwrap(), None); - assert_eq!(calculate_blake2b_length_str("256").unwrap(), Some(32)); + assert_eq!( + calculate_blake_length_str(AlgoKind::Blake2b, "0").unwrap(), + None + ); + assert!(calculate_blake_length_str(AlgoKind::Blake2b, "10").is_err()); + assert!(calculate_blake_length_str(AlgoKind::Blake2b, "520").is_err()); + assert_eq!( + calculate_blake_length_str(AlgoKind::Blake2b, "512").unwrap(), + None + ); + assert_eq!( + calculate_blake_length_str(AlgoKind::Blake2b, "256").unwrap(), + Some(32) + ); } } diff --git a/src/uucore/src/lib/features/checksum/validate.rs b/src/uucore/src/lib/features/checksum/validate.rs index 17b1c7bf522..fcfba47e8c0 100644 --- a/src/uucore/src/lib/features/checksum/validate.rs +++ b/src/uucore/src/lib/features/checksum/validate.rs @@ -614,6 +614,7 @@ fn identify_algo_name_and_length( algo_name_input: Option, last_algo: &mut Option, ) -> Result<(AlgoKind, Option), LineCheckError> { + use AlgoKind as ak; let algo_from_line = line_info.algo_name.clone().unwrap_or_default(); let Ok(line_algo) = AlgoKind::from_cksum(algo_from_line.to_lowercase()) else { // Unknown algorithm @@ -629,24 +630,20 @@ fn identify_algo_name_and_length( match (algo_name_input, line_algo) { (l, r) if l == r => (), // Edge case for SHA2, which matches SHA(224|256|384|512) - ( - AlgoKind::Sha2, - AlgoKind::Sha224 | AlgoKind::Sha256 | AlgoKind::Sha384 | AlgoKind::Sha512, - ) => (), + (ak::Sha2, ak::Sha224 | ak::Sha256 | ak::Sha384 | ak::Sha512) => (), _ => return Err(LineCheckError::ImproperlyFormatted), } } let bytes = if let Some(bitlen) = line_info.algo_bit_len { match line_algo { - AlgoKind::Blake2b if bitlen % 8 == 0 => Some(bitlen / 8), - AlgoKind::Sha2 | AlgoKind::Sha3 if [224, 256, 384, 512].contains(&bitlen) => { - Some(bitlen) - } - AlgoKind::Shake128 | AlgoKind::Shake256 => Some(bitlen), + ak::Blake2b | ak::Blake3 if bitlen % 8 == 0 => Some(bitlen / 8), + ak::Sha2 | ak::Sha3 if [224, 256, 384, 512].contains(&bitlen) => Some(bitlen), + ak::Shake128 | ak::Shake256 => Some(bitlen), // Either - // the algo based line is provided with a bit length - // with an algorithm that does not support it (only Blake2B does). + // the algo based line is provided with a bit length with an + // algorithm that does not support it (only Blake2b, Blake3, sha2, + // and sha3 do). // // eg: MD5-128 (foo.txt) = fffffffff // ^ This is illegal @@ -654,9 +651,12 @@ fn identify_algo_name_and_length( // the given length is wrong because it's not a multiple of 8. _ => return Err(LineCheckError::ImproperlyFormatted), } - } else if line_algo == AlgoKind::Blake2b { + } else if line_algo == ak::Blake2b { // Default length with BLAKE2b, Some(64) + } else if line_algo == ak::Blake3 { + // Default length with BLAKE3, + Some(32) } else { None }; @@ -741,7 +741,7 @@ fn process_algo_based_line( // If the digest bitlen is known, we can check the format of the expected // checksum with it. let digest_char_length_hint = match (algo_kind, algo_byte_len) { - (AlgoKind::Blake2b, Some(byte_len)) => Some(byte_len), + (AlgoKind::Blake2b | AlgoKind::Blake3, Some(byte_len)) => Some(byte_len), (AlgoKind::Shake128 | AlgoKind::Shake256, Some(bit_len)) => Some(bit_len.div_ceil(8)), (AlgoKind::Shake128, None) => Some(sum::Shake128::DEFAULT_BIT_SIZE.div_ceil(8)), (AlgoKind::Shake256, None) => Some(sum::Shake256::DEFAULT_BIT_SIZE.div_ceil(8)), @@ -764,6 +764,7 @@ fn process_non_algo_based_line( cli_algo_length: Option, opts: ChecksumValidateOptions, ) -> Result<(), LineCheckError> { + use AlgoKind as ak; let mut filename_to_check = line_info.filename.as_slice(); if filename_to_check.starts_with(b"*") && line_number == 0 @@ -778,16 +779,16 @@ fn process_non_algo_based_line( // When a specific algorithm name is input, use it and use the provided // bits except when dealing with blake2b, sha2 and sha3, where we will // detect the length. - let (algo_kind, algo_byte_len) = match cli_algo_kind { - AlgoKind::Blake2b => (AlgoKind::Blake2b, Some(expected_checksum.len())), - algo @ (AlgoKind::Sha2 | AlgoKind::Sha3) => { + let algo_byte_len = match cli_algo_kind { + ak::Blake2b | ak::Blake3 => Some(expected_checksum.len()), + ak::Sha2 | ak::Sha3 => { // multiplication by 8 to get the number of bits - (algo, Some(expected_checksum.len() * 8)) + Some(expected_checksum.len() * 8) } - _ => (cli_algo_kind, cli_algo_length), + _ => cli_algo_length, }; - let algo = SizedAlgoKind::from_unsized(algo_kind, algo_byte_len)?; + let algo = SizedAlgoKind::from_unsized(cli_algo_kind, algo_byte_len)?; compute_and_check_digest_from_file(filename_to_check, &expected_checksum, algo, opts) } diff --git a/src/uucore/src/lib/features/sum.rs b/src/uucore/src/lib/features/sum.rs index 279643a962b..af122ced1a3 100644 --- a/src/uucore/src/lib/features/sum.rs +++ b/src/uucore/src/lib/features/sum.rs @@ -122,25 +122,48 @@ impl Digest for Blake2b { } } -#[derive(Default)] -pub struct Blake3(blake3::Hasher); +pub struct Blake3 { + digest: blake3::Hasher, + byte_size: usize, +} + +impl Blake3 { + /// Default length for the BLAKE3 digest in bytes. + pub const DEFAULT_BYTE_SIZE: usize = 32; + + pub fn with_output_bytes(output_bytes: usize) -> Self { + Self { + digest: blake3::Hasher::new(), + byte_size: output_bytes, + } + } +} + +impl Default for Blake3 { + fn default() -> Self { + Self { + digest: blake3::Hasher::default(), + byte_size: Self::DEFAULT_BYTE_SIZE, + } + } +} impl Digest for Blake3 { fn hash_update(&mut self, input: &[u8]) { - self.0.update(input); + self.digest.update(input); } fn hash_finalize(&mut self, out: &mut [u8]) { - let hash_result = &self.0.finalize(); - out.copy_from_slice(hash_result.as_bytes()); + let mut hash_result = self.digest.finalize_xof(); + hash_result.fill(out); } fn reset(&mut self) { - *self = Self::default(); + *self = Self::with_output_bytes(self.output_bytes()); } fn output_bits(&self) -> usize { - 256 + self.byte_size * 8 } } From 1c9c9e47421b22a0a8b2dfd44a9a58fa8dd29a42 Mon Sep 17 00:00:00 2001 From: Dorian Peron Date: Sun, 8 Feb 2026 13:37:00 +0100 Subject: [PATCH 015/264] test(cksum): Test for cksum -a blake3 --- Cargo.lock | 12 ++++ Cargo.toml | 2 + tests/by-util/test_cksum.rs | 110 ++++++++++++++++++++++++++++++++++++ 3 files changed, 124 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 6237a36609f..e3919220ddb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -559,6 +559,7 @@ dependencies = [ "regex", "rlimit", "rstest", + "rstest_reuse", "rustc-hash", "selinux", "sha1", @@ -2558,6 +2559,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "rstest_reuse" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3a8fb4672e840a587a66fc577a5491375df51ddb88f2a2c2a792598c326fe14" +dependencies = [ + "quote", + "rand 0.8.5", + "syn", +] + [[package]] name = "rust-ini" version = "0.21.3" diff --git a/Cargo.toml b/Cargo.toml index 92013b0af57..9ef80cd072c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -432,6 +432,7 @@ rayon = "1.10" regex = "1.10.4" rlimit = "0.11.0" rstest = "0.26.0" +rstest_reuse = "0.7.0" rustc-hash = "2.1.1" rust-ini = "0.21.0" same-file = "1.0.6" @@ -640,6 +641,7 @@ uucore = { workspace = true, features = [ walkdir.workspace = true hex-literal = "1.0.0" rstest.workspace = true +rstest_reuse.workspace = true [target.'cfg(unix)'.dev-dependencies] nix = { workspace = true, features = [ diff --git a/tests/by-util/test_cksum.rs b/tests/by-util/test_cksum.rs index 67f96e90762..1ab5bb45285 100644 --- a/tests/by-util/test_cksum.rs +++ b/tests/by-util/test_cksum.rs @@ -5,6 +5,7 @@ // spell-checker:ignore (words) asdf algo algos asha mgmt xffname hexa GFYEQ HYQK Yqxb dont checkfile use rstest::rstest; +use rstest_reuse::{apply, template}; use uutests::at_and_ucmd; use uutests::new_ucmd; @@ -3287,3 +3288,112 @@ fn test_check_shake256_no_length() { .fails() .stderr_only("cksum: 'standard input': no properly formatted checksum lines found\n"); } + +#[template] +#[rstest] +#[case::no_length( + b"foo", + "04e0bb39f30b1a3feb89f536c93be15055482df748674b00d26e5a75777702e9", + None +)] +#[case( + b"foo", + "04e0bb39f30b1a3feb89f536c93be15055482df748674b00d26e5a75777702e9", + Some(0) +)] +#[case( + b"foo", + "04e0bb39f30b1a3feb89f536c93be15055482df748674b00d26e5a75777702e9", + Some(256) +)] +#[case( + b"foo", + "04e0bb39f30b1a3feb89f536c93be15055482df748674b00d26e5a75777702e9791074b7511b59d31c71c62f5a745689fa6c", + Some(400) +)] +#[case(b"foo", "04e0bb39f3", Some(40))] +#[case(b"foo", "04e0", Some(16))] +#[case(b"foo", "04", Some(8))] +fn test_blake3(#[case] input: &[u8], #[case] expected: &str, #[case] length: Option) {} + +#[apply(test_blake3)] +fn test_compute_blake3( + #[case] input: &[u8], + #[case] expected: &str, + #[case] length: Option, +) { + let length_args: &[String] = if let Some(len) = length { + &["-l".into(), len.to_string()] + } else { + &[] + }; + + new_ucmd!() + .arg("-a") + .arg("blake3") + .args(length_args) + .pipe_in(input) + .succeeds() + .stdout_only(format!( + "BLAKE3{} (-) = {expected}\n", + match length { + Some(0) | None => "-256".into(), + Some(i) => format!("-{i}"), + } + )); + + // with --untagged + new_ucmd!() + .arg("-a") + .arg("blake3") + .arg("--untagged") + .args(length_args) + .pipe_in(input) + .succeeds() + .stdout_only(format!("{expected} -\n")); +} + +#[apply(test_blake3)] +fn test_check_blake3_tagged( + #[case] input: &[u8], + #[case] digest: &str, + #[case] opt_len: Option, +) { + let (at, mut ucmd) = at_and_ucmd!(); + at.write_bytes("FILE", input); + + let len = match opt_len { + Some(0) => "-256".into(), + Some(i) => format!("-{i}"), + None => String::new(), + }; + + let tagged = format!("BLAKE3{len} (FILE) = {digest}",); + + ucmd.arg("-c") + .arg("-a") + .arg("blake3") + .pipe_in(tagged) + .succeeds() + .stdout_only("FILE: OK\n"); +} + +#[apply(test_blake3)] +#[allow(clippy::used_underscore_binding)] +fn test_check_blake3_untagged( + #[case] input: &[u8], + #[case] digest: &str, + #[case] _opt_len: Option, +) { + let (at, mut ucmd) = at_and_ucmd!(); + at.write_bytes("FILE", input); + + let untagged = format!("{digest} FILE"); + + ucmd.arg("-c") + .arg("-a") + .arg("blake3") + .pipe_in(untagged) + .succeeds() + .stdout_only("FILE: OK\n"); +} From 4129d99a7e18c68cb253c7233d6bf50479b42fcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dorian=20P=C3=A9ron?= Date: Fri, 13 Mar 2026 17:09:31 +0100 Subject: [PATCH 016/264] bench(cksum): Re-enable blake3 benchmarks --- src/uu/cksum/benches/cksum_bench.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uu/cksum/benches/cksum_bench.rs b/src/uu/cksum/benches/cksum_bench.rs index 5c2d3d9c614..81f70bbb3d1 100644 --- a/src/uu/cksum/benches/cksum_bench.rs +++ b/src/uu/cksum/benches/cksum_bench.rs @@ -104,7 +104,7 @@ bench_algorithm!(cksum_sha224, "sha224"); bench_algorithm!(cksum_sha256, "sha256"); bench_algorithm!(cksum_sha384, "sha384"); bench_algorithm!(cksum_sha512, "sha512"); -// broken. benchmarking error messages issues/10002 bench_algorithm!(cksum_blake3, "blake3"); +bench_algorithm!(cksum_blake3, "blake3"); bench_shake_algorithm!(cksum_shake128, "shake128", Shake128); bench_shake_algorithm!(cksum_shake256, "shake256", Shake256); From 0fc85a26a54347959c83dd4bb381291ad1dbb683 Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Sat, 21 Mar 2026 13:38:00 +0100 Subject: [PATCH 017/264] ls: add header to new files --- src/uu/ls/src/config.rs | 5 +++++ src/uu/ls/src/display.rs | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/src/uu/ls/src/config.rs b/src/uu/ls/src/config.rs index 035b83fd779..12d14238f4d 100644 --- a/src/uu/ls/src/config.rs +++ b/src/uu/ls/src/config.rs @@ -1,3 +1,8 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + // spell-checker:ignore (ToDO) somegroup nlink tabsize dired subdired dtype colorterm stringly // spell-checker:ignore nohash strtime clocale diff --git a/src/uu/ls/src/display.rs b/src/uu/ls/src/display.rs index e080b1ad1fd..4f77ebd8720 100644 --- a/src/uu/ls/src/display.rs +++ b/src/uu/ls/src/display.rs @@ -1,3 +1,8 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + // spell-checker:ignore (ToDO) somegroup nlink tabsize dired subdired dtype colorterm stringly // spell-checker:ignore nohash strtime clocale From 303a0f32a23313501fa57d69ea8bda45ac93073c Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Tue, 17 Mar 2026 00:34:51 +0900 Subject: [PATCH 018/264] l10n.yml: drop cargo build covered by cargo {test,run} --- .github/workflows/l10n.yml | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/.github/workflows/l10n.yml b/.github/workflows/l10n.yml index 57c4d51ef21..30b2cd5b17e 100644 --- a/.github/workflows/l10n.yml +++ b/.github/workflows/l10n.yml @@ -62,11 +62,6 @@ jobs: brew install coreutils ;; esac - - name: Build with platform features - shell: bash - run: | - ## Build with platform-specific features to enable l10n functionality - cargo build --features ${{ matrix.job.features }} - name: Test l10n functionality shell: bash run: | @@ -149,10 +144,6 @@ jobs: sudo apt-get -y update ; sudo apt-get -y install libselinux1-dev sudo locale-gen --keep-existing fr_FR.UTF-8 locale -a | grep -i fr || exit 1 - - name: Build coreutils with clap localization support - shell: bash - run: | - cargo build --features feat_os_unix --bin coreutils - name: Test English clap error localization shell: bash run: | @@ -321,11 +312,6 @@ jobs: ## Generate French locale for testing sudo locale-gen --keep-existing fr_FR.UTF-8 locale -a | grep -i fr || echo "French locale not found, continuing anyway" - - name: Build coreutils with l10n support - shell: bash - run: | - ## Build coreutils with Unix features and l10n support - cargo build --features feat_os_unix --bin coreutils - name: Test French localization shell: bash run: | @@ -706,7 +692,7 @@ jobs: mkdir -p "$CARGO_INSTALL_DIR" # Install using cargo with l10n features - cargo install --path . --features ${{ matrix.job.features }} --root "$CARGO_INSTALL_DIR" --locked + cargo install --path . --features "ls,cat,touch" --root "$CARGO_INSTALL_DIR" --locked # Verify installation echo "Testing cargo-installed binaries..." @@ -1379,11 +1365,3 @@ jobs: echo "::warning::More locales than expected ($total_match_count entries)" echo "This might be expected for utility + uucore locales" fi - - l10n_locale_embedding_regression_test: - name: L10n/Locale Embedding Regression Test - runs-on: ubuntu-latest - needs: [l10n_locale_embedding_cat, l10n_locale_embedding_ls, l10n_locale_embedding_multicall, l10n_locale_embedding_cargo_install] - steps: - - name: All locale embedding tests passed - run: echo "✓ All locale embedding tests passed successfully" From 991b1286c591946b1d882522f215e10dd273f7e7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 22 Mar 2026 01:02:29 +0000 Subject: [PATCH 019/264] chore(deps): update rust crate zip to v8.3.1 --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e3919220ddb..3c112f6da87 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5181,9 +5181,9 @@ dependencies = [ [[package]] name = "zip" -version = "8.3.0" +version = "8.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a243cfad17427fc077f529da5a95abe4e94fd2bfdb601611870a6557cc67657" +checksum = "5c546feb4481b0fbafb4ef0d79b6204fc41c6f9884b1b73b1d73f82442fc0845" dependencies = [ "crc32fast", "flate2", From 908c0bc54f8e7df9cdff3a0ae21e6839fde399c2 Mon Sep 17 00:00:00 2001 From: "Guillem L. Jara" <4lon3ly0@tutanota.com> Date: Sat, 21 Mar 2026 19:32:52 +0100 Subject: [PATCH 020/264] fix(ls): reduce allocations on PathData struct --- src/uu/ls/src/colors.rs | 2 +- src/uu/ls/src/display.rs | 4 +-- src/uu/ls/src/ls.rs | 64 ++++++++++++++++++++++++---------------- 3 files changed, 41 insertions(+), 29 deletions(-) diff --git a/src/uu/ls/src/colors.rs b/src/uu/ls/src/colors.rs index 197fd2c8cec..66dd4f6a2ef 100644 --- a/src/uu/ls/src/colors.rs +++ b/src/uu/ls/src/colors.rs @@ -530,7 +530,7 @@ pub(crate) fn color_name( let has_capabilities = style_manager .colors .has_explicit_style_for(Indicator::Capabilities) - && uucore::fsxattr::has_security_cap_acl(path.p_buf.as_path()); + && uucore::fsxattr::has_security_cap_acl(&path.p_buf); // If the file has capabilities, use a specific style for `ca` (capabilities) if has_capabilities { diff --git a/src/uu/ls/src/display.rs b/src/uu/ls/src/display.rs index 4f77ebd8720..dcbc68a3537 100644 --- a/src/uu/ls/src/display.rs +++ b/src/uu/ls/src/display.rs @@ -749,9 +749,9 @@ fn display_item_name( match fs::canonicalize(&absolute_target) { Ok(resolved_target) => { let target_data = PathData::new( - resolved_target, + resolved_target.as_path().into(), None, - target_path.file_name().map(OsStr::to_os_string), + target_path.file_name().map(Cow::Borrowed), config, false, ); diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 913bc04abd8..5da6d62bdc1 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -784,45 +784,54 @@ pub fn uu_app() -> Command { .after_help(translate!("ls-after-help")) } +#[derive(Debug)] +enum PathDataDisplayName<'a> { + SelfReferential, + Moo(Cow<'a, OsStr>), +} + /// Represents a Path along with it's associated data. /// Any data that will be reused several times makes sense to be added to this structure. /// Caching data here helps eliminate redundant syscalls to fetch same information. #[derive(Debug)] -struct PathData { +struct PathData<'a> { // Result got from symlink_metadata() or metadata() based on config md: OnceCell>, ft: OnceCell>, // can be used to avoid reading the filetype. Can be also called d_type: // https://www.gnu.org/software/libc/manual/html_node/Directory-Entries.html - de: RefCell>>, + de: RefCell>, security_context: OnceCell>, // Name of the file - will be empty for . or .. - display_name: OsString, + display_name: PathDataDisplayName<'a>, // PathBuf that all above data corresponds to - p_buf: PathBuf, + p_buf: Cow<'a, Path>, must_dereference: bool, command_line: bool, } -impl PathData { +impl<'a> PathData<'a> { fn new( - p_buf: PathBuf, + p_buf: Cow<'a, Path>, dir_entry: Option, - file_name: Option, + file_name: Option>, config: &Config, command_line: bool, ) -> Self { // We cannot use `Path::ends_with` or `Path::Components`, because they remove occurrences of '.' // For '..', the filename is None let display_name = if let Some(name) = file_name { - name + PathDataDisplayName::Moo(name) } else if command_line { - p_buf.as_os_str().to_os_string() + PathDataDisplayName::SelfReferential } else { - dir_entry - .as_ref() - .map(DirEntry::file_name) - .unwrap_or_default() + PathDataDisplayName::Moo( + dir_entry + .as_ref() + .map(DirEntry::file_name) + .unwrap_or_default() + .into(), + ) }; let must_dereference = match &config.dereference { @@ -848,7 +857,7 @@ impl PathData { let md: OnceCell> = OnceCell::new(); let security_context: OnceCell> = OnceCell::new(); - let de: RefCell>> = if let Some(de) = dir_entry { + let de: RefCell> = if let Some(de) = dir_entry { if must_dereference { if let Ok(md_pb) = p_buf.metadata() { md.get_or_init(|| Some(md_pb.clone())); @@ -860,7 +869,7 @@ impl PathData { ft.get_or_init(|| Some(ft_de)); } - RefCell::new(Some(de.into())) + RefCell::new(Some(de)) } else { RefCell::new(None) }; @@ -881,7 +890,7 @@ impl PathData { self.md .get_or_init(|| { if !self.must_dereference { - if let Some(dir_entry) = RefCell::take(&self.de).as_deref() { + if let Some(dir_entry) = RefCell::take(&self.de) { return dir_entry.metadata().ok(); } } @@ -941,11 +950,14 @@ impl PathData { } fn display_name(&self) -> &OsStr { - &self.display_name + match self.display_name { + PathDataDisplayName::SelfReferential => self.p_buf.as_os_str(), + PathDataDisplayName::Moo(ref cow) => cow, + } } } -impl Colorable for PathData { +impl Colorable for PathData<'_> { fn file_name(&self) -> OsString { self.display_name().to_os_string() } @@ -1006,7 +1018,7 @@ pub fn list(locs: Vec<&Path>, config: &Config) -> UResult<()> { }; for loc in locs { - let path_data = PathData::new(PathBuf::from(loc), None, None, config, true); + let path_data = PathData::new(loc.into(), None, None, config, true); // Getting metadata here is no big deal as it's just the CWD // and we really just want to know if the strings exist as files/dirs @@ -1169,7 +1181,7 @@ fn sort_entries(entries: &mut [PathData], config: &Config) { !match ft { None => { // If it metadata cannot be determined, treat as a file. - get_metadata_with_deref_opt(p.p_buf.as_path(), true) + get_metadata_with_deref_opt(&p.p_buf, true) .map_or_else(|_| false, |m| m.is_dir()) } Some(ft) => ft.is_dir(), @@ -1186,7 +1198,7 @@ fn depth_first_list( dired: &mut DiredOutput, is_top_level: bool, ) -> UResult<()> { - let path_data = PathData::new(dir_path, None, None, config, false); + let path_data = PathData::new(dir_path.as_path().into(), None, None, config, false); // Print dir heading - name... 'total' comes after error display if state.initial_locs_len > 1 || config.recursive { @@ -1228,16 +1240,16 @@ fn depth_first_list( const DOT_DIRECTORIES: usize = 2; let v = vec![ PathData::new( - path_data.path().to_path_buf(), + path_data.path().into(), None, - Some(".".into()), + Some(OsStr::new(".").into()), config, false, ), PathData::new( - path_data.path().join(".."), + path_data.path().join("..").into(), None, - Some("..".into()), + Some(OsStr::new("..").into()), config, false, ), @@ -1253,7 +1265,7 @@ fn depth_first_list( Ok(dir_entry) => { if should_display(&dir_entry, config) { buf.push(PathData::new( - dir_entry.path(), + dir_entry.path().into(), Some(dir_entry), None, config, From 12554dd55e5f4d02001f0aa635a4e3e0efa2ed4f Mon Sep 17 00:00:00 2001 From: "Guillem L. Jara" <4lon3ly0@tutanota.com> Date: Sat, 21 Mar 2026 19:45:52 +0100 Subject: [PATCH 021/264] fix(ls): reduce allocations on sorting Switch to unstable sorting as it is in-place, and remove an unnecessary string conversion. --- src/uu/ls/src/ls.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 5da6d62bdc1..3069ff151aa 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -1128,7 +1128,7 @@ pub fn list(locs: Vec<&Path>, config: &Config) -> UResult<()> { fn sort_entries(entries: &mut [PathData], config: &Config) { match config.sort { - Sort::Time => entries.sort_by_key(|k| { + Sort::Time => entries.sort_unstable_by_key(|k| { Reverse( k.metadata() .and_then(|md| metadata_get_time(md, config.time)) @@ -1136,24 +1136,24 @@ fn sort_entries(entries: &mut [PathData], config: &Config) { ) }), Sort::Size => { - entries.sort_by_key(|k| Reverse(k.metadata().map_or(0, Metadata::len))); + entries.sort_unstable_by_key(|k| Reverse(k.metadata().map_or(0, Metadata::len))); } // The default sort in GNU ls is case insensitive - Sort::Name => entries.sort_by(|a, b| a.display_name().cmp(b.display_name())), - Sort::Version => entries.sort_by(|a, b| { + Sort::Name => entries.sort_unstable_by(|a, b| a.display_name().cmp(b.display_name())), + Sort::Version => entries.sort_unstable_by(|a, b| { version_cmp( os_str_as_bytes_lossy(a.path().as_os_str()).as_ref(), os_str_as_bytes_lossy(b.path().as_os_str()).as_ref(), ) - .then(a.path().to_string_lossy().cmp(&b.path().to_string_lossy())) + .then(a.path().cmp(b.path())) }), - Sort::Extension => entries.sort_by(|a, b| { + Sort::Extension => entries.sort_unstable_by(|a, b| { a.path() .extension() .cmp(&b.path().extension()) .then(a.path().file_stem().cmp(&b.path().file_stem())) }), - Sort::Width => entries.sort_by(|a, b| { + Sort::Width => entries.sort_unstable_by(|a, b| { a.display_name() .len() .cmp(&b.display_name().len()) @@ -1167,7 +1167,7 @@ fn sort_entries(entries: &mut [PathData], config: &Config) { } if config.group_directories_first && config.sort != Sort::None { - entries.sort_by_key(|p| { + entries.sort_unstable_by_key(|p| { let ft = { // We will always try to deref symlinks to group directories, so PathData.md // is not always useful. From abd8df3b7133173f2e6b7cb6f0e6247996d99464 Mon Sep 17 00:00:00 2001 From: "Guillem L. Jara" <4lon3ly0@tutanota.com> Date: Sat, 21 Mar 2026 20:10:55 +0100 Subject: [PATCH 022/264] fix(ls): remove various redundant clones --- src/uu/ls/src/ls.rs | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 3069ff151aa..6e42dbbda0c 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -860,8 +860,8 @@ impl<'a> PathData<'a> { let de: RefCell> = if let Some(de) = dir_entry { if must_dereference { if let Ok(md_pb) = p_buf.metadata() { - md.get_or_init(|| Some(md_pb.clone())); ft.get_or_init(|| Some(md_pb.file_type())); + md.get_or_init(|| Some(md_pb)); } } @@ -1430,7 +1430,7 @@ fn get_security_context<'a>( "{}", translate!( "ls-warning-failed-to-get-security-context", - "path" => path.quote().to_string() + "path" => path.quote() ) ); return Cow::Borrowed(SUBSTITUTE_STRING); @@ -1441,18 +1441,10 @@ fn get_security_context<'a>( let context = context.strip_suffix(&[0]).unwrap_or(context); - let res: String = String::from_utf8(context.to_vec()).unwrap_or_else(|e| { - show_warning!( - "{}", - translate!( - "ls-warning-getting-security-context", - "path" => path.quote().to_string(), - "error" => e.to_string() - ) - ); - - String::from_utf8_lossy(context).to_string() - }); + let res: String = match str::from_utf8(context) { + Ok(s) => s.to_string(), + Err(_) => String::from_utf8_lossy(context).into_owned(), + }; return Cow::Owned(res); } From 60eb952ae997d0d3f03f0d106516cdb5dfa6b4b7 Mon Sep 17 00:00:00 2001 From: "Guillem L. Jara" <4lon3ly0@tutanota.com> Date: Sat, 21 Mar 2026 20:12:13 +0100 Subject: [PATCH 023/264] fix(ls): reduce duplicated time syscalls --- src/uu/ls/src/ls.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 6e42dbbda0c..3bbf834b578 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -999,6 +999,7 @@ pub fn list(locs: Vec<&Path>, config: &Config) -> UResult<()> { let mut dirs = Vec::::new(); let mut dired = DiredOutput::default(); let initial_locs_len = locs.len(); + let now = SystemTime::now(); let mut state = ListState { out: BufWriter::new(stdout()), @@ -1010,8 +1011,7 @@ pub fn list(locs: Vec<&Path>, config: &Config) -> UResult<()> { // Time range for which to use the "recent" format. Anything from 0.5 year in the past to now // (files with modification time in the future use "old" format). // According to GNU a Gregorian year has 365.2425 * 24 * 60 * 60 == 31556952 seconds on the average. - recent_time_range: (SystemTime::now() - Duration::new(31_556_952 / 2, 0)) - ..=SystemTime::now(), + recent_time_range: (now - Duration::new(31_556_952 / 2, 0))..=now, stack: Vec::new(), listed_ancestors: FxHashSet::default(), initial_locs_len, From 232dbf38326614d993e3f768d7b18c88953bee35 Mon Sep 17 00:00:00 2001 From: "Guillem L. Jara" <4lon3ly0@tutanota.com> Date: Sat, 21 Mar 2026 22:15:34 +0100 Subject: [PATCH 024/264] fix(ls): optimize create_hyperlink and make it loseless. --- src/uu/ls/src/display.rs | 51 +++++++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/src/uu/ls/src/display.rs b/src/uu/ls/src/display.rs index dcbc68a3537..d6cf209f646 100644 --- a/src/uu/ls/src/display.rs +++ b/src/uu/ls/src/display.rs @@ -11,6 +11,7 @@ use std::cell::LazyCell; use std::os::unix::fs::{FileTypeExt, MetadataExt}; #[cfg(windows)] use std::os::windows::fs::MetadataExt; +use std::sync::LazyLock; /// Show the directory name in the case where several arguments are given to ls use std::{borrow::Cow, iter}; use std::{ @@ -1120,37 +1121,43 @@ fn classify_file(path: &PathData) -> Option { } fn create_hyperlink(name: &OsStr, path: &PathData) -> OsString { - let hostname = hostname::get().unwrap_or_else(|_| OsString::from("")); - let hostname = hostname.to_string_lossy(); + static HOSTNAME: LazyLock = LazyLock::new(|| hostname::get().unwrap_or_default()); - let absolute_path = fs::canonicalize(path.path()).unwrap_or_default(); + // OSC 8 hyperlink format: \x1b]8;;URL\x1b\\TEXT\x1b]8;;\x1b\\ + // \x1b = ESC, \x1b\\ = ESC backslash + // FIXME: switch to constants once OsStr::new() is const-stable and over our MSRV. + let osc_8_head = OsStr::new("\x1b]8;;file://"); + let osc_8_tail = OsStr::new("\x1b]8;;\x1b\\"); + let esc_bl = OsStr::new("\x1b\\"); - // Get bytes for URL encoding in a cross-platform way - let absolute_path_bytes = os_str_as_bytes_lossy(absolute_path.as_os_str()); + let absolute_path = fs::canonicalize(path.path()).unwrap_or_default(); + let mut ret = OsString::with_capacity( + osc_8_head.len() + + osc_8_tail.len() + + HOSTNAME.len() + + esc_bl.len() + + absolute_path.as_os_str().len(), + ); + ret.push(osc_8_head); + ret.push(HOSTNAME.as_os_str()); // a set of safe ASCII bytes that don't need encoding #[cfg(not(target_os = "windows"))] - let unencoded_bytes = b"_-.~/"; + let unencoded = |c| matches!(c, '_' | '-' | '.' | '~' | '/'); #[cfg(target_os = "windows")] - let unencoded_bytes = b"_-.~/\\:"; + let unencoded = |c| matches!(c, '_' | '-' | '.' | '~' | '/' | '\\' | ':'); - // Encode at byte level to properly handle UTF-8 sequences and preserve invalid UTF-8 - let full_encoded_path: String = absolute_path_bytes - .iter() - .map(|&b: &u8| { - if b.is_ascii_alphanumeric() || unencoded_bytes.contains(&b) { - (b as char).to_string() - } else { - format!("%{b:02x}") - } - }) - .collect(); + for &b in absolute_path.as_os_str().as_encoded_bytes() { + if b.is_ascii_alphanumeric() || unencoded(b as char) { + let _ = ret.write_char(b as char); + } else { + let _ = write!(ret, "%{b:02x}"); + } + } - // OSC 8 hyperlink format: \x1b]8;;URL\x1b\\TEXT\x1b]8;;\x1b\\ - // \x1b = ESC, \x1b\\ = ESC backslash - let mut ret: OsString = format!("\x1b]8;;file://{hostname}{full_encoded_path}\x1b\\").into(); + ret.push(esc_bl); ret.push(name); - ret.push("\x1b]8;;\x1b\\"); + ret.push(osc_8_tail); ret } From 27fd852e3d85a5504d11dbe42ba6c74e5e59f014 Mon Sep 17 00:00:00 2001 From: "Guillem L. Jara" <4lon3ly0@tutanota.com> Date: Sat, 21 Mar 2026 22:24:04 +0100 Subject: [PATCH 025/264] fix(ls): do not allocate for symlink len check --- src/uu/ls/src/display.rs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/uu/ls/src/display.rs b/src/uu/ls/src/display.rs index d6cf209f646..764b09f4227 100644 --- a/src/uu/ls/src/display.rs +++ b/src/uu/ls/src/display.rs @@ -4,7 +4,7 @@ // file that was distributed with this source code. // spell-checker:ignore (ToDO) somegroup nlink tabsize dired subdired dtype colorterm stringly -// spell-checker:ignore nohash strtime clocale +// spell-checker:ignore nohash strtime clocale ilog use std::cell::LazyCell; #[cfg(unix)] @@ -253,8 +253,12 @@ fn display_dir_entry_size( } SizeOrDeviceId::Size(size) => (size.len(), 0usize, 0usize), }; + #[cfg(unix)] + let nlink_len = digits(md.nlink()); + #[cfg(not(unix))] + let nlink_len = display_symlink_count(md).len(); ( - display_symlink_count(md).len(), + nlink_len, display_uname(md, config, state).len(), display_group(md, config, state).len(), size_len, @@ -266,6 +270,11 @@ fn display_dir_entry_size( } } +#[cfg(unix)] +fn digits(num: u64) -> usize { + (num.checked_ilog10().unwrap_or(0) + 1) as usize +} + // A simple, performant, ExtendPad trait to add a string to a Vec, padding with spaces // on the left or right, without making additional copies, or using formatting functions. pub trait ExtendPad { @@ -1218,7 +1227,7 @@ fn calculate_padding_collection( #[cfg(unix)] if config.inode { let inode_len = if let Some(md) = item.metadata() { - display_inode(md).len() + digits(md.ino()) } else { continue; }; From 1569d0d9d912f048d9daa666b1cfce34668dce44 Mon Sep 17 00:00:00 2001 From: "Guillem L. Jara" <4lon3ly0@tutanota.com> Date: Sat, 21 Mar 2026 22:39:18 +0100 Subject: [PATCH 026/264] fix(ls): fix double-lookup of (COLOR)TERM env vars Also switches to var_os for alloc-free code. --- src/uu/ls/src/config.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/uu/ls/src/config.rs b/src/uu/ls/src/config.rs index 12d14238f4d..eb4b1f0bf5b 100644 --- a/src/uu/ls/src/config.rs +++ b/src/uu/ls/src/config.rs @@ -342,17 +342,22 @@ fn extract_time(options: &clap::ArgMatches) -> MetadataTimeField { /// Some env variables can be passed /// For now, we are only verifying if empty or not and known for `TERM` fn is_color_compatible_term() -> bool { - let is_term_set = std::env::var("TERM").is_ok(); - let is_colorterm_set = std::env::var("COLORTERM").is_ok(); + let term = std::env::var_os("TERM"); + let colorterm = std::env::var_os("COLORTERM"); + let is_term_set = term.is_some(); + let is_colorterm_set = colorterm.is_some(); - let term = std::env::var("TERM").unwrap_or_default(); - let colorterm = std::env::var("COLORTERM").unwrap_or_default(); + let term = term.unwrap_or_default(); + let colorterm = colorterm.unwrap_or_default(); // Search function in the TERM struct to manage the wildcards - let term_matches = |term: &str| -> bool { + let term_matches = |term: &OsStr| -> bool { uucore::colors::TERMS.iter().any(|&pattern| { term == pattern - || (pattern.ends_with('*') && term.starts_with(&pattern[..pattern.len() - 1])) + || (pattern.ends_with('*') + && term + .as_encoded_bytes() + .starts_with(&pattern.as_bytes()[..pattern.len() - 1])) }) }; From 4bccb7b59ff82f934fea788ab619cbb46cf83637 Mon Sep 17 00:00:00 2001 From: "Guillem L. Jara" <4lon3ly0@tutanota.com> Date: Sat, 21 Mar 2026 22:46:42 +0100 Subject: [PATCH 027/264] fix(ls): fix double-allocation of block options --- src/uu/ls/src/config.rs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/uu/ls/src/config.rs b/src/uu/ls/src/config.rs index eb4b1f0bf5b..8118070c4e7 100644 --- a/src/uu/ls/src/config.rs +++ b/src/uu/ls/src/config.rs @@ -697,17 +697,9 @@ impl Config { let hyperlink = extract_hyperlink(options); let opt_block_size = options.get_one::(options::size::BLOCK_SIZE); - let opt_si = opt_block_size.is_some() - && options - .get_one::(options::size::BLOCK_SIZE) - .unwrap() - .eq("si") + let opt_si = opt_block_size.is_some_and(|x| x == options::size::SI) || options.get_flag(options::size::SI); - let opt_hr = (opt_block_size.is_some() - && options - .get_one::(options::size::BLOCK_SIZE) - .unwrap() - .eq("human-readable")) + let opt_hr = opt_block_size.is_some_and(|x| x == options::size::HUMAN_READABLE) || options.get_flag(options::size::HUMAN_READABLE); let opt_kb = options.get_flag(options::size::KIBIBYTES); From 73bc8a88b8d12a37639608ab54a5e8214cee82df Mon Sep 17 00:00:00 2001 From: "Guillem L. Jara" <4lon3ly0@tutanota.com> Date: Sat, 21 Mar 2026 22:56:06 +0100 Subject: [PATCH 028/264] fix(ls): use Cow on parse_time_style to reduce allocations --- src/uu/ls/src/config.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/uu/ls/src/config.rs b/src/uu/ls/src/config.rs index 8118070c4e7..f775daafdb2 100644 --- a/src/uu/ls/src/config.rs +++ b/src/uu/ls/src/config.rs @@ -7,6 +7,7 @@ // spell-checker:ignore nohash strtime clocale use std::{ + borrow::Cow, ffi::{OsStr, OsString}, io::{IsTerminal, stdout}, num::IntErrorKind, @@ -1029,8 +1030,8 @@ fn parse_time_style(options: &clap::ArgMatches) -> Result<(String, Option(options::TIME_STYLE) - .map(ToOwned::to_owned) - .or_else(|| std::env::var("TIME_STYLE").ok()) + .map(|s| Cow::Borrowed(s.as_str())) + .or_else(|| std::env::var("TIME_STYLE").ok().map(Cow::Owned)) { //If both FULL_TIME and TIME_STYLE are present //The one added last is dominant From a5be6a76926e3df5aa7e6092a7c1f739e655660f Mon Sep 17 00:00:00 2001 From: "Guillem L. Jara" <4lon3ly0@tutanota.com> Date: Sat, 21 Mar 2026 23:04:23 +0100 Subject: [PATCH 029/264] fix(ls): remove various useless LineEnding::to_string() --- src/uu/ls/src/display.rs | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/src/uu/ls/src/display.rs b/src/uu/ls/src/display.rs index 764b09f4227..7d8def48315 100644 --- a/src/uu/ls/src/display.rs +++ b/src/uu/ls/src/display.rs @@ -47,7 +47,6 @@ use uucore::{ format::human::human_readable, fs::display_permissions, fsext::metadata_get_time, - line_ending::LineEnding, os_str_as_bytes_lossy, quoting_style::{QuotingStyle, locale_aware_escape_dir_name, locale_aware_escape_name}, show, @@ -557,8 +556,8 @@ fn display_additional_leading_info( result } -fn calculate_line_len(output_len: usize, item_len: usize, line_ending: LineEnding) -> usize { - output_len + item_len + line_ending.to_string().len() +fn calculate_line_len(output_len: usize, item_len: usize) -> usize { + output_len + item_len + 1 // line ending } #[cfg(unix)] @@ -976,13 +975,7 @@ fn display_item_long( dired_name_len += 1; } let displayed_len = item_display.displayed.len() + usize::from(needs_space); - update_dired_for_item( - dired, - output_display.len(), - displayed_len, - dired_name_len, - config.line_ending, - ); + update_dired_for_item(dired, output_display.len(), displayed_len, dired_name_len); } let item_name = item_display.displayed; @@ -995,7 +988,7 @@ fn display_item_long( }; write_os_str(&mut output_display, &displayed_item)?; - output_display.extend(config.line_ending.to_string().as_bytes()); + output_display.push(config.line_ending as u8); } else { #[cfg(unix)] let leading_char = { @@ -1090,12 +1083,11 @@ fn display_item_long( output_display.len(), displayed_item.displayed.len(), displayed_item.dired_name_len, - config.line_ending, ); } let displayed_item = displayed_item.displayed; write_os_str(&mut output_display, &displayed_item)?; - output_display.extend(config.line_ending.to_string().as_bytes()); + output_display.push(config.line_ending as u8); } state.out.write_all(&output_display)?; @@ -1189,9 +1181,8 @@ fn update_dired_for_item( output_display_len: usize, displayed_len: usize, dired_name_len: usize, - line_ending: LineEnding, ) { - let line_len = calculate_line_len(output_display_len, displayed_len, line_ending); + let line_len = calculate_line_len(output_display_len, displayed_len); dired::calculate_and_update_positions(dired, output_display_len, dired_name_len, line_len); } From 9dfb1c0739d6574b3fde2cfe9fbe309e00484c01 Mon Sep 17 00:00:00 2001 From: "Guillem L. Jara" <4lon3ly0@tutanota.com> Date: Sat, 21 Mar 2026 23:07:05 +0100 Subject: [PATCH 030/264] fix(ls): do not alloc char to push to OsString in display_item_name --- src/uu/ls/src/display.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uu/ls/src/display.rs b/src/uu/ls/src/display.rs index 7d8def48315..c0f6461d023 100644 --- a/src/uu/ls/src/display.rs +++ b/src/uu/ls/src/display.rs @@ -727,7 +727,7 @@ fn display_item_name( }; if let Some(c) = char_opt { - name.push(OsStr::new(&c.to_string())); + let _ = name.write_char(c); } } From 5a823fb8e433e9c9a33ce16d7752c6ec5dd0cc53 Mon Sep 17 00:00:00 2001 From: "Guillem L. Jara" <4lon3ly0@tutanota.com> Date: Sat, 21 Mar 2026 23:14:46 +0100 Subject: [PATCH 031/264] fix(ls): various inode optimizations Remove format machinery, an unnecessary allocation and a duplicated function. --- src/uu/ls/src/display.rs | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/uu/ls/src/display.rs b/src/uu/ls/src/display.rs index c0f6461d023..6775ce477fe 100644 --- a/src/uu/ls/src/display.rs +++ b/src/uu/ls/src/display.rs @@ -531,7 +531,7 @@ fn display_additional_leading_info( { if config.inode { let i = if let Some(md) = item.metadata() { - get_inode(md) + display_inode(md) } else { "?".to_owned() }; @@ -540,10 +540,10 @@ fn display_additional_leading_info( } if config.alloc_size { - let s = if let Some(md) = item.metadata() { - display_size(get_block_size(md, config), config) + let s: Cow<'_, str> = if let Some(md) = item.metadata() { + display_size(get_block_size(md, config), config).into() } else { - "?".to_owned() + "?".into() }; // extra space is insert to align the sizes, as needed for all formats, except for the comma format. if config.format == Format::Commas { @@ -560,11 +560,6 @@ fn calculate_line_len(output_len: usize, item_len: usize) -> usize { output_len + item_len + 1 // line ending } -#[cfg(unix)] -fn get_inode(metadata: &Metadata) -> String { - format!("{}", metadata.ino()) -} - // Currently getpwuid is `linux` target only. If it's broken state.out into // a posix-compliant attribute this can be updated... #[cfg(unix)] @@ -662,7 +657,7 @@ pub fn display_size(size: u64, config: &Config) -> String { /// /// This function relies on the following parameters in the provided `&Config`: /// * `config.quoting_style` to decide how we will escape `name` using [`locale_aware_escape_name`]. -/// * `config.inode` decides whether to display inode numbers beside names using [`get_inode`]. +/// * `config.inode` decides whether to display inode numbers beside names using [`display_inode`]. /// * `config.color` decides whether it's going to color `name` using [`color_name`]. /// * `config.indicator_style` to append specific characters to `name` using [`classify_file`]. /// * `config.format` to display symlink targets if `Format::Long`. This function is also @@ -839,7 +834,7 @@ fn display_item_name( /// This writes to the [`BufWriter`] `state.out` a single string of the output of `ls -l`. /// /// It writes the following keys, in order: -/// * `inode` ([`get_inode`], config-optional) +/// * `inode` ([`display_inode`], config-optional) /// * `permissions` ([`display_permissions`]) /// * `symlink_count` ([`display_symlink_count`]) /// * `owner` ([`display_uname`], config-optional) @@ -1193,7 +1188,7 @@ fn display_symlink_count(metadata: &Metadata) -> String { #[cfg(unix)] fn display_inode(metadata: &Metadata) -> String { - get_inode(metadata) + metadata.ino().to_string() } #[cfg(unix)] From 8fb9261bde39030f41c8bc208c57fbc4939597bb Mon Sep 17 00:00:00 2001 From: "Guillem L. Jara" <4lon3ly0@tutanota.com> Date: Sat, 21 Mar 2026 23:26:18 +0100 Subject: [PATCH 032/264] fix(ls): preallocate for a couple of vectors/strings --- src/uu/ls/src/display.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/uu/ls/src/display.rs b/src/uu/ls/src/display.rs index 6775ce477fe..731ca24b403 100644 --- a/src/uu/ls/src/display.rs +++ b/src/uu/ls/src/display.rs @@ -164,7 +164,7 @@ fn escape_name_with_locale(name: &OsStr, config: &Config) -> OsString { fn locale_quote(name: &OsStr, style: LocaleQuoting) -> OsString { let bytes = os_str_as_bytes_lossy(name); - let mut quoted = String::new(); + let mut quoted = String::with_capacity(name.len() + 2); match style { LocaleQuoting::Single => quoted.push('\''), LocaleQuoting::Double => quoted.push('"'), @@ -355,7 +355,7 @@ pub fn display_items( write!(state.out, "{}", style_manager.apply_normal())?; } - let mut names_vec = Vec::new(); + let mut names_vec = Vec::with_capacity(items.len()); #[cfg(unix)] let should_display_leading_info = config.inode || config.alloc_size; @@ -820,7 +820,9 @@ fn display_item_name( }; let old_name = name; - name = format!("{security_context} ").into(); + name = OsString::with_capacity(security_context.len() + 1 + old_name.len()); + name.push(security_context); + name.push(" "); name.push(old_name); } } From ac411913b2c6fb0a30fb8e2ce2f00ae857a378f4 Mon Sep 17 00:00:00 2001 From: "Guillem L. Jara" <4lon3ly0@tutanota.com> Date: Sun, 22 Mar 2026 00:34:22 +0100 Subject: [PATCH 033/264] fix(ls): reuse intermediate buffer on --long --- src/uu/ls/src/display.rs | 175 +++++++++++++++++++++++---------------- src/uu/ls/src/ls.rs | 15 ++++ 2 files changed, 120 insertions(+), 70 deletions(-) diff --git a/src/uu/ls/src/display.rs b/src/uu/ls/src/display.rs index 731ca24b403..2c77d417139 100644 --- a/src/uu/ls/src/display.rs +++ b/src/uu/ls/src/display.rs @@ -6,12 +6,14 @@ // spell-checker:ignore (ToDO) somegroup nlink tabsize dired subdired dtype colorterm stringly // spell-checker:ignore nohash strtime clocale ilog +use core::ops::RangeInclusive; use std::cell::LazyCell; #[cfg(unix)] use std::os::unix::fs::{FileTypeExt, MetadataExt}; #[cfg(windows)] use std::os::windows::fs::MetadataExt; use std::sync::LazyLock; +use std::time::SystemTime; /// Show the directory name in the case where several arguments are given to ls use std::{borrow::Cow, iter}; use std::{ @@ -23,6 +25,8 @@ use std::{ use ansi_width::ansi_width; use glob::MatchOptions; +#[cfg(unix)] +use rustc_hash::FxHashMap; use term_grid::{DEFAULT_SEPARATOR_SIZE, Direction, Filling, Grid, GridOptions}; #[cfg(unix)] @@ -53,7 +57,7 @@ use uucore::{ time::{FormatSystemTimeFallback, format_system_time}, }; -use crate::colors::color_name; +use crate::colors::{StyleManager, color_name}; use crate::config::Files; use crate::dired::{self, DiredOutput}; use crate::{Config, ListState, LsError, PathData, get_block_size}; @@ -258,8 +262,8 @@ fn display_dir_entry_size( let nlink_len = display_symlink_count(md).len(); ( nlink_len, - display_uname(md, config, state).len(), - display_group(md, config, state).len(), + display_uname(md, config, &mut state.uid_cache).len(), + display_group(md, config, &mut state.gid_cache).len(), size_len, major_len, minor_len, @@ -377,7 +381,7 @@ pub fn display_items( config, prefix_context, more_info, - state, + state.style_manager.as_mut(), LazyCell::new(Box::new(|| 0)), ); @@ -563,10 +567,14 @@ fn calculate_line_len(output_len: usize, item_len: usize) -> usize { // Currently getpwuid is `linux` target only. If it's broken state.out into // a posix-compliant attribute this can be updated... #[cfg(unix)] -fn display_uname<'a>(metadata: &Metadata, config: &Config, state: &'a mut ListState) -> &'a String { +fn display_uname<'a>( + metadata: &Metadata, + config: &Config, + uid_cache: &'a mut FxHashMap, +) -> &'a String { let uid = metadata.uid(); - state.uid_cache.entry(uid).or_insert_with(|| { + uid_cache.entry(uid).or_insert_with(|| { if config.long.numeric_uid_gid { uid.to_string() } else { @@ -576,9 +584,13 @@ fn display_uname<'a>(metadata: &Metadata, config: &Config, state: &'a mut ListSt } #[cfg(unix)] -fn display_group<'a>(metadata: &Metadata, config: &Config, state: &'a mut ListState) -> &'a String { +fn display_group<'a>( + metadata: &Metadata, + config: &Config, + gid_cache: &'a mut FxHashMap, +) -> &'a String { let gid = metadata.gid(); - state.gid_cache.entry(gid).or_insert_with(|| { + gid_cache.entry(gid).or_insert_with(|| { if config.long.numeric_uid_gid { gid.to_string() } else { @@ -588,19 +600,19 @@ fn display_group<'a>(metadata: &Metadata, config: &Config, state: &'a mut ListSt } #[cfg(not(unix))] -fn display_uname(_metadata: &Metadata, _config: &Config, _state: &mut ListState) -> &'static str { +fn display_uname(_metadata: &Metadata, _config: &Config, _uid_cache: &mut ()) -> &'static str { "somebody" } #[cfg(not(unix))] -fn display_group(_metadata: &Metadata, _config: &Config, _state: &mut ListState) -> &'static str { +fn display_group(_metadata: &Metadata, _config: &Config, _gid_cache: &mut ()) -> &'static str { "somegroup" } fn display_date( metadata: &Metadata, config: &Config, - state: &mut ListState, + recent_time_range: &RangeInclusive, out: &mut Vec, ) -> UResult<()> { let Some(time) = metadata_get_time(metadata, config.time) else { @@ -611,7 +623,7 @@ fn display_date( // Use "recent" format if the given date is considered recent (i.e., in the last 6 months), // or if no "older" format is available. let fmt = match &config.time_format_older { - Some(time_format_older) if !state.recent_time_range.contains(&time) => time_format_older, + Some(time_format_older) if !recent_time_range.contains(&time) => time_format_older, _ => &config.time_format_recent, }; @@ -673,7 +685,7 @@ fn display_item_name( config: &Config, prefix_context: Option, more_info: Option, - state: &mut ListState, + mut style_manager: Option<&mut StyleManager>, current_column: LazyCell usize + '_>>, ) -> DisplayItemName { // This is our return value. We start by `&path.display_name` and modify it along the way. @@ -686,7 +698,7 @@ fn display_item_name( name = create_hyperlink(&name, path); } - if let Some(style_manager) = &mut state.style_manager { + if let Some(style_manager) = style_manager.as_mut() { let len = name.len(); name = color_name(name, path, style_manager, None, is_wrap(len)); } @@ -739,7 +751,7 @@ fn display_item_name( // We might as well color the symlink output after the arrow. // This makes extra system calls, but provides important information that // people run `ls -l --color` are very interested in. - if let Some(style_manager) = &mut state.style_manager { + if let Some(style_manager) = &mut style_manager { let escaped_target = escape_name_with_locale(target_path.as_os_str(), config); // We get the absolute path to be able to construct PathData with valid Metadata. // This is because relative symlinks will fail to get_metadata. @@ -870,14 +882,14 @@ fn display_item_long( dired: &mut DiredOutput, quoted: bool, ) -> UResult<()> { - let mut output_display: Vec = Vec::with_capacity(128); - // apply normal color to non filename outputs if let Some(style_manager) = &mut state.style_manager { - output_display.extend(style_manager.apply_normal().as_bytes()); + state + .display_buf + .extend(style_manager.apply_normal().as_bytes()); } if config.dired { - output_display.extend(b" "); + state.display_buf.extend(b" "); } if let Some(md) = item.metadata() { #[cfg(any(not(unix), target_os = "android", target_os = "macos"))] @@ -885,49 +897,64 @@ fn display_item_long( let is_acl_set = false; #[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] let is_acl_set = has_acl(item.path()); - output_display.extend(display_permissions(md, true).as_bytes()); + state + .display_buf + .extend(display_permissions(md, true).as_bytes()); if item.security_context(config).len() > 1 { // GNU `ls` uses a "." character to indicate a file with a security context, // but not other alternate access method. - output_display.extend(b"."); + state.display_buf.extend(b"."); } else if is_acl_set { - output_display.extend(b"+"); + state.display_buf.extend(b"+"); } else { - output_display.extend(b" "); + state.display_buf.extend(b" "); } - output_display.extend_pad_left(&display_symlink_count(md), padding.link_count); + state + .display_buf + .extend_pad_left(&display_symlink_count(md), padding.link_count); if config.long.owner { - output_display.extend(b" "); - output_display.extend_pad_right(display_uname(md, config, state), padding.uname); + state.display_buf.extend(b" "); + state.display_buf.extend_pad_right( + display_uname(md, config, &mut state.uid_cache), + padding.uname, + ); } if config.long.group { - output_display.extend(b" "); - output_display.extend_pad_right(display_group(md, config, state), padding.group); + state.display_buf.extend(b" "); + state.display_buf.extend_pad_right( + display_group(md, config, &mut state.gid_cache), + padding.group, + ); } if config.context { - output_display.extend(b" "); - output_display.extend_pad_right(item.security_context(config), padding.context); + state.display_buf.extend(b" "); + state + .display_buf + .extend_pad_right(item.security_context(config), padding.context); } // Author is only different from owner on GNU/Hurd, so we reuse // the owner, since GNU/Hurd is not currently supported by Rust. if config.long.author { - output_display.extend(b" "); - output_display.extend_pad_right(display_uname(md, config, state), padding.uname); + state.display_buf.extend(b" "); + state.display_buf.extend_pad_right( + display_uname(md, config, &mut state.uid_cache), + padding.uname, + ); } match display_len_or_rdev(md, config) { SizeOrDeviceId::Size(size) => { - output_display.extend(b" "); - output_display.extend_pad_left(&size, padding.size); + state.display_buf.extend(b" "); + state.display_buf.extend_pad_left(&size, padding.size); } SizeOrDeviceId::Device(major, minor) => { - output_display.extend(b" "); - output_display.extend_pad_left( + state.display_buf.extend(b" "); + state.display_buf.extend_pad_left( &major, #[cfg(not(unix))] 0usize, @@ -938,8 +965,8 @@ fn display_item_long( .saturating_sub(padding.minor.saturating_add(2usize)), ), ); - output_display.extend(b", "); - output_display.extend_pad_left( + state.display_buf.extend(b", "); + state.display_buf.extend_pad_left( &minor, #[cfg(not(unix))] 0usize, @@ -949,18 +976,18 @@ fn display_item_long( } } - output_display.extend(b" "); - display_date(md, config, state, &mut output_display)?; - output_display.extend(b" "); + state.display_buf.extend(b" "); + display_date(md, config, &state.recent_time_range, &mut state.display_buf)?; + state.display_buf.extend(b" "); let item_display = display_item_name( item, config, None, None, - state, + state.style_manager.as_mut(), LazyCell::new(Box::new(|| { - ansi_width(&String::from_utf8_lossy(&output_display)) + ansi_width(&String::from_utf8_lossy(&state.display_buf)) })), ); @@ -972,7 +999,12 @@ fn display_item_long( dired_name_len += 1; } let displayed_len = item_display.displayed.len() + usize::from(needs_space); - update_dired_for_item(dired, output_display.len(), displayed_len, dired_name_len); + update_dired_for_item( + dired, + state.display_buf.len(), + displayed_len, + dired_name_len, + ); } let item_name = item_display.displayed; @@ -984,8 +1016,8 @@ fn display_item_long( item_name }; - write_os_str(&mut output_display, &displayed_item)?; - output_display.push(config.line_ending as u8); + write_os_str(&mut state.display_buf, &displayed_item)?; + state.display_buf.push(config.line_ending as u8); } else { #[cfg(unix)] let leading_char = { @@ -1024,36 +1056,38 @@ fn display_item_long( } }; - output_display.extend(leading_char.as_bytes()); - output_display.extend(b"?????????"); + state.display_buf.extend(leading_char.as_bytes()); + state.display_buf.extend(b"?????????"); if item.security_context(config).len() > 1 { // GNU `ls` uses a "." character to indicate a file with a security context, // but not other alternate access method. - output_display.extend(b"."); + state.display_buf.extend(b"."); } - output_display.extend(b" "); - output_display.extend_pad_left("?", padding.link_count); + state.display_buf.extend(b" "); + state.display_buf.extend_pad_left("?", padding.link_count); if config.long.owner { - output_display.extend(b" "); - output_display.extend_pad_right("?", padding.uname); + state.display_buf.extend(b" "); + state.display_buf.extend_pad_right("?", padding.uname); } if config.long.group { - output_display.extend(b" "); - output_display.extend_pad_right("?", padding.group); + state.display_buf.extend(b" "); + state.display_buf.extend_pad_right("?", padding.group); } if config.context { - output_display.extend(b" "); - output_display.extend_pad_right(item.security_context(config), padding.context); + state.display_buf.extend(b" "); + state + .display_buf + .extend_pad_right(item.security_context(config), padding.context); } // Author is only different from owner on GNU/Hurd, so we reuse // the owner, since GNU/Hurd is not currently supported by Rust. if config.long.author { - output_display.extend(b" "); - output_display.extend_pad_right("?", padding.uname); + state.display_buf.extend(b" "); + state.display_buf.extend_pad_right("?", padding.uname); } let displayed_item = display_item_name( @@ -1061,32 +1095,33 @@ fn display_item_long( config, None, None, - state, + state.style_manager.as_mut(), LazyCell::new(Box::new(|| { - ansi_width(&String::from_utf8_lossy(&output_display)) + ansi_width(&String::from_utf8_lossy(&state.display_buf)) })), ); let date_len = 12; - output_display.extend(b" "); - output_display.extend_pad_left("?", padding.size); - output_display.extend(b" "); - output_display.extend_pad_left("?", date_len); - output_display.extend(b" "); + state.display_buf.extend(b" "); + state.display_buf.extend_pad_left("?", padding.size); + state.display_buf.extend(b" "); + state.display_buf.extend_pad_left("?", date_len); + state.display_buf.extend(b" "); if config.dired { update_dired_for_item( dired, - output_display.len(), + state.display_buf.len(), displayed_item.displayed.len(), displayed_item.dired_name_len, ); } let displayed_item = displayed_item.displayed; - write_os_str(&mut output_display, &displayed_item)?; - output_display.push(config.line_ending as u8); + write_os_str(&mut state.display_buf, &displayed_item)?; + state.display_buf.push(config.line_ending as u8); } - state.out.write_all(&output_display)?; + state.out.write_all(&state.display_buf)?; + state.display_buf.clear(); Ok(()) } diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 3bbf834b578..75d72a3e8b8 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -975,6 +975,7 @@ impl Colorable for PathData<'_> { type DirData = (PathBuf, bool); // A struct to encapsulate state that is passed around from `list` functions. +#[cfg_attr(not(unix), allow(dead_code))] struct ListState<'a> { out: BufWriter, style_manager: Option>, @@ -987,10 +988,15 @@ struct ListState<'a> { uid_cache: FxHashMap, #[cfg(unix)] gid_cache: FxHashMap, + #[cfg(not(unix))] + uid_cache: (), + #[cfg(not(unix))] + gid_cache: (), recent_time_range: RangeInclusive, stack: Vec, listed_ancestors: FxHashSet, initial_locs_len: usize, + display_buf: Vec, } #[allow(clippy::cognitive_complexity)] @@ -1008,6 +1014,10 @@ pub fn list(locs: Vec<&Path>, config: &Config) -> UResult<()> { uid_cache: FxHashMap::default(), #[cfg(unix)] gid_cache: FxHashMap::default(), + #[cfg(not(unix))] + uid_cache: (), + #[cfg(not(unix))] + gid_cache: (), // Time range for which to use the "recent" format. Anything from 0.5 year in the past to now // (files with modification time in the future use "old" format). // According to GNU a Gregorian year has 365.2425 * 24 * 60 * 60 == 31556952 seconds on the average. @@ -1015,6 +1025,11 @@ pub fn list(locs: Vec<&Path>, config: &Config) -> UResult<()> { stack: Vec::new(), listed_ancestors: FxHashSet::default(), initial_locs_len, + display_buf: Vec::with_capacity(if config.format == Format::Long { + 128 + } else { + 0 + }), }; for loc in locs { From 368ae59d21c40a3916a27d6610e4d9e04d60d653 Mon Sep 17 00:00:00 2001 From: "Guillem L. Jara" <4lon3ly0@tutanota.com> Date: Sun, 22 Mar 2026 00:51:39 +0100 Subject: [PATCH 034/264] fix(ls): use Vec::push where appropiate in display_item_long Should improve codegen, especially in debug mode. --- src/uu/ls/src/display.rs | 69 ++++++++++++++++++++-------------------- 1 file changed, 35 insertions(+), 34 deletions(-) diff --git a/src/uu/ls/src/display.rs b/src/uu/ls/src/display.rs index 2c77d417139..19fe4d90eb8 100644 --- a/src/uu/ls/src/display.rs +++ b/src/uu/ls/src/display.rs @@ -903,11 +903,11 @@ fn display_item_long( if item.security_context(config).len() > 1 { // GNU `ls` uses a "." character to indicate a file with a security context, // but not other alternate access method. - state.display_buf.extend(b"."); + state.display_buf.push(b'.'); } else if is_acl_set { - state.display_buf.extend(b"+"); + state.display_buf.push(b'+'); } else { - state.display_buf.extend(b" "); + state.display_buf.push(b' '); } state @@ -915,7 +915,7 @@ fn display_item_long( .extend_pad_left(&display_symlink_count(md), padding.link_count); if config.long.owner { - state.display_buf.extend(b" "); + state.display_buf.push(b' '); state.display_buf.extend_pad_right( display_uname(md, config, &mut state.uid_cache), padding.uname, @@ -923,7 +923,7 @@ fn display_item_long( } if config.long.group { - state.display_buf.extend(b" "); + state.display_buf.push(b' '); state.display_buf.extend_pad_right( display_group(md, config, &mut state.gid_cache), padding.group, @@ -931,7 +931,7 @@ fn display_item_long( } if config.context { - state.display_buf.extend(b" "); + state.display_buf.push(b' '); state .display_buf .extend_pad_right(item.security_context(config), padding.context); @@ -940,7 +940,7 @@ fn display_item_long( // Author is only different from owner on GNU/Hurd, so we reuse // the owner, since GNU/Hurd is not currently supported by Rust. if config.long.author { - state.display_buf.extend(b" "); + state.display_buf.push(b' '); state.display_buf.extend_pad_right( display_uname(md, config, &mut state.uid_cache), padding.uname, @@ -949,11 +949,11 @@ fn display_item_long( match display_len_or_rdev(md, config) { SizeOrDeviceId::Size(size) => { - state.display_buf.extend(b" "); + state.display_buf.push(b' '); state.display_buf.extend_pad_left(&size, padding.size); } SizeOrDeviceId::Device(major, minor) => { - state.display_buf.extend(b" "); + state.display_buf.push(b' '); state.display_buf.extend_pad_left( &major, #[cfg(not(unix))] @@ -976,9 +976,9 @@ fn display_item_long( } } - state.display_buf.extend(b" "); + state.display_buf.push(b' '); display_date(md, config, &state.recent_time_range, &mut state.display_buf)?; - state.display_buf.extend(b" "); + state.display_buf.push(b' '); let item_display = display_item_name( item, @@ -1009,7 +1009,8 @@ fn display_item_long( let item_name = item_display.displayed; let displayed_item = if needs_space { - let mut ret: OsString = " ".into(); + let mut ret = OsString::with_capacity(item_name.len() + 1); + let _ = ret.write_char(' '); ret.push(&item_name); ret } else { @@ -1023,61 +1024,61 @@ fn display_item_long( let leading_char = { if let Some(ft) = item.file_type() { if ft.is_char_device() { - "c" + 'c' } else if ft.is_block_device() { - "b" + 'b' } else if ft.is_symlink() { - "l" + 'l' } else if ft.is_dir() { - "d" + 'd' } else { - "-" + '-' } } else if item.is_dangling_link() { - "l" + 'l' } else { - "-" + '-' } }; #[cfg(not(unix))] let leading_char = { if let Some(ft) = item.file_type() { if ft.is_symlink() { - "l" + 'l' } else if ft.is_dir() { - "d" + 'd' } else { - "-" + '-' } } else if item.is_dangling_link() { - "l" + 'l' } else { - "-" + '-' } }; - state.display_buf.extend(leading_char.as_bytes()); + state.display_buf.push(leading_char as u8); state.display_buf.extend(b"?????????"); if item.security_context(config).len() > 1 { // GNU `ls` uses a "." character to indicate a file with a security context, // but not other alternate access method. - state.display_buf.extend(b"."); + state.display_buf.push(b'.'); } - state.display_buf.extend(b" "); + state.display_buf.push(b' '); state.display_buf.extend_pad_left("?", padding.link_count); if config.long.owner { - state.display_buf.extend(b" "); + state.display_buf.push(b' '); state.display_buf.extend_pad_right("?", padding.uname); } if config.long.group { - state.display_buf.extend(b" "); + state.display_buf.push(b' '); state.display_buf.extend_pad_right("?", padding.group); } if config.context { - state.display_buf.extend(b" "); + state.display_buf.push(b' '); state .display_buf .extend_pad_right(item.security_context(config), padding.context); @@ -1086,7 +1087,7 @@ fn display_item_long( // Author is only different from owner on GNU/Hurd, so we reuse // the owner, since GNU/Hurd is not currently supported by Rust. if config.long.author { - state.display_buf.extend(b" "); + state.display_buf.push(b' '); state.display_buf.extend_pad_right("?", padding.uname); } @@ -1102,11 +1103,11 @@ fn display_item_long( ); let date_len = 12; - state.display_buf.extend(b" "); + state.display_buf.push(b' '); state.display_buf.extend_pad_left("?", padding.size); - state.display_buf.extend(b" "); + state.display_buf.push(b' '); state.display_buf.extend_pad_left("?", date_len); - state.display_buf.extend(b" "); + state.display_buf.push(b' '); if config.dired { update_dired_for_item( From 6a4febe8fecb5a5a932a1ff4dff3450c9a4a6edf Mon Sep 17 00:00:00 2001 From: "Guillem L. Jara" <4lon3ly0@tutanota.com> Date: Sun, 22 Mar 2026 00:54:48 +0100 Subject: [PATCH 035/264] fix(ls): do not dynamically dispatch display_item_name closures --- src/uu/ls/src/display.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/uu/ls/src/display.rs b/src/uu/ls/src/display.rs index 19fe4d90eb8..c2f32d0d2b1 100644 --- a/src/uu/ls/src/display.rs +++ b/src/uu/ls/src/display.rs @@ -382,7 +382,7 @@ pub fn display_items( prefix_context, more_info, state.style_manager.as_mut(), - LazyCell::new(Box::new(|| 0)), + LazyCell::new(|| 0), ); names_vec.push(cell.displayed); @@ -686,7 +686,7 @@ fn display_item_name( prefix_context: Option, more_info: Option, mut style_manager: Option<&mut StyleManager>, - current_column: LazyCell usize + '_>>, + current_column: LazyCell usize>, ) -> DisplayItemName { // This is our return value. We start by `&path.display_name` and modify it along the way. let mut name = escape_name_with_locale(path.display_name(), config); @@ -986,9 +986,7 @@ fn display_item_long( None, None, state.style_manager.as_mut(), - LazyCell::new(Box::new(|| { - ansi_width(&String::from_utf8_lossy(&state.display_buf)) - })), + LazyCell::new(|| ansi_width(&String::from_utf8_lossy(&state.display_buf))), ); let needs_space = quoted && !os_str_starts_with(&item_display.displayed, b"'"); @@ -1097,9 +1095,7 @@ fn display_item_long( None, None, state.style_manager.as_mut(), - LazyCell::new(Box::new(|| { - ansi_width(&String::from_utf8_lossy(&state.display_buf)) - })), + LazyCell::new(|| ansi_width(&String::from_utf8_lossy(&state.display_buf))), ); let date_len = 12; From fb28daec3fc5bf664c410994ba821b8378811335 Mon Sep 17 00:00:00 2001 From: "Guillem L. Jara" <4lon3ly0@tutanota.com> Date: Sun, 22 Mar 2026 01:09:34 +0100 Subject: [PATCH 036/264] fix(ls): do not unnecessary format & allocate on return_total --- src/uu/ls/src/ls.rs | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 75d72a3e8b8..bf6f5bcb66f 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -1300,10 +1300,9 @@ fn depth_first_list( sort_entries(buf, config); if config.format == Format::Long || config.alloc_size { - let total = return_total(buf, config, &mut state.out)?; - write!(state.out, "{}", total.as_str())?; + let total = write_total(buf, config, &mut state.out)?; if config.dired { - dired::add_total(dired, total.len()); + dired::add_total(dired, total); } } @@ -1351,11 +1350,7 @@ fn get_metadata_with_deref_opt(p_buf: &Path, dereference: bool) -> std::io::Resu } } -fn return_total( - items: &[PathData], - config: &Config, - out: &mut BufWriter, -) -> UResult { +fn write_total(items: &[PathData], config: &Config, out: &mut BufWriter) -> UResult { let mut total_size = 0; for item in items { total_size += item @@ -1366,11 +1361,10 @@ fn return_total( if config.dired { dired::indent(out)?; } - Ok(format!( - "{}{}", - translate!("ls-total", "size" => display_size(total_size, config)), - config.line_ending - )) + let total = translate!("ls-total", "size" => display_size(total_size, config)); + out.write_all(total.as_bytes())?; + out.write_all(&[config.line_ending as u8])?; + Ok(total.len() + 1) } #[allow(unused_variables)] From 0a11492f7401f8cb36da3a67a53462c31748319e Mon Sep 17 00:00:00 2001 From: "Guillem L. Jara" <4lon3ly0@tutanota.com> Date: Sun, 22 Mar 2026 01:39:51 +0100 Subject: [PATCH 037/264] fix(ls): remove unnecessary allocations in display_additional_leading_info --- src/uu/ls/src/display.rs | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/src/uu/ls/src/display.rs b/src/uu/ls/src/display.rs index c2f32d0d2b1..101708720bf 100644 --- a/src/uu/ls/src/display.rs +++ b/src/uu/ls/src/display.rs @@ -9,6 +9,8 @@ use core::ops::RangeInclusive; use std::cell::LazyCell; #[cfg(unix)] +use std::fmt::Display; +#[cfg(unix)] use std::os::unix::fs::{FileTypeExt, MetadataExt}; #[cfg(windows)] use std::os::windows::fs::MetadataExt; @@ -18,7 +20,7 @@ use std::time::SystemTime; use std::{borrow::Cow, iter}; use std::{ ffi::{OsStr, OsString}, - fmt::Write as _, + fmt::Write as FmtWrite, fs::{self, DirEntry, FileType, Metadata}, io::{BufWriter, Stdout, Write}, }; @@ -333,9 +335,7 @@ pub fn display_items( let should_display_leading_info = config.alloc_size; if should_display_leading_info { - let more_info = display_additional_leading_info(item, &padding_collection, config); - - write!(state.out, "{more_info}")?; + display_additional_leading_info(item, &padding_collection, config, &mut state.out)?; } display_item_long(item, &padding_collection, config, state, dired, quoted)?; @@ -368,7 +368,9 @@ pub fn display_items( for i in items { let more_info = if should_display_leading_info { - Some(display_additional_leading_info(i, &padding, config)) + let mut s = Vec::new(); + display_additional_leading_info(i, &padding, config, &mut s)?; + Some(String::from_utf8(s).unwrap()) // Should always be UTF-8 } else { None }; @@ -529,17 +531,17 @@ fn display_additional_leading_info( item: &PathData, padding: &PaddingCollection, config: &Config, -) -> String { - let mut result = String::new(); + out: &mut impl Write, +) -> UResult<()> { #[cfg(unix)] { if config.inode { - let i = if let Some(md) = item.metadata() { - display_inode(md) + let inode = padding.inode; + if let Some(md) = item.metadata() { + write!(out, "{:>inode$} ", display_inode(md))?; } else { - "?".to_owned() - }; - write!(result, "{} ", pad_left(&i, padding.inode)).unwrap(); + write!(out, "{:>inode$} ", '?')?; + } } } @@ -551,13 +553,15 @@ fn display_additional_leading_info( }; // extra space is insert to align the sizes, as needed for all formats, except for the comma format. if config.format == Format::Commas { - write!(result, "{s} ").unwrap(); + out.write_all(s.as_bytes())?; + out.write_all(b" ")?; } else { - write!(result, "{} ", pad_left(&s, padding.block_size)).unwrap(); + let block_size = padding.block_size; + write!(out, "{s:>block_size$} ")?; } } - result + Ok(()) } fn calculate_line_len(output_len: usize, item_len: usize) -> usize { @@ -1221,7 +1225,7 @@ fn display_symlink_count(metadata: &Metadata) -> String { } #[cfg(unix)] -fn display_inode(metadata: &Metadata) -> String { +fn display_inode(metadata: &Metadata) -> impl Display { metadata.ino().to_string() } From 038f7786681e0ecc2229fb875dd483a5d92ba74c Mon Sep 17 00:00:00 2001 From: "Guillem L. Jara" <4lon3ly0@tutanota.com> Date: Sun, 22 Mar 2026 02:32:34 +0100 Subject: [PATCH 038/264] fix(ls): remove double collect and extra allocations in display_grid --- src/uu/ls/src/display.rs | 47 +++++++++++++++++----------------------- 1 file changed, 20 insertions(+), 27 deletions(-) diff --git a/src/uu/ls/src/display.rs b/src/uu/ls/src/display.rs index 101708720bf..f2ad0d81c44 100644 --- a/src/uu/ls/src/display.rs +++ b/src/uu/ls/src/display.rs @@ -471,40 +471,33 @@ fn display_grid( writeln!(out)?; } } else { - let names: Vec<_> = if quoted { - // In case some names are quoted, GNU adds a space before each - // entry that does not start with a quote to make it prettier - // on multiline. - // - // Example: - // ``` - // $ ls - // 'a\nb' bar - // foo baz - // ^ ^ - // These spaces is added - // ``` + let names: Vec = { + let mut buf = Vec::new(); names .map(|n| { - if os_str_starts_with(&n, b"'") || os_str_starts_with(&n, b"\"") { - n - } else { - let mut ret: OsString = " ".into(); - ret.push(n); - ret + // In case some names are quoted, GNU adds a space before each + // entry that does not start with a quote to make it prettier + // on multiline. + // + // Example: + // ``` + // $ ls + // 'a\nb' bar + // foo baz + // ^ ^ + // These spaces is added + // ``` + // FIXME: the Grid crate only supports &str, so can't display raw bytes + buf.clear(); + if quoted && !os_str_starts_with(&n, b"'") && !os_str_starts_with(&n, b"\"") { + buf.push(b' '); } + buf.extend(n.as_encoded_bytes()); + String::from_utf8_lossy(&buf).into_owned() }) .collect() - } else { - names.collect() }; - // FIXME: the Grid crate only supports &str, so can't display raw bytes - let names: Vec<_> = names - .into_iter() - .map(|s| s.to_string_lossy().into_owned()) - .collect(); - // Since tab_size=0 means no \t, use Spaces separator for optimization. let filling = match tab_size { 0 => Filling::Spaces(DEFAULT_SEPARATOR_SIZE), From afe988e102e6fbb00f1494242c969ef70980f0e3 Mon Sep 17 00:00:00 2001 From: "Guillem L. Jara" <4lon3ly0@tutanota.com> Date: Sun, 22 Mar 2026 02:55:33 +0100 Subject: [PATCH 039/264] fix(ls): remove unnecessary allocations in display_item_name --- src/uu/ls/src/display.rs | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/uu/ls/src/display.rs b/src/uu/ls/src/display.rs index f2ad0d81c44..1695eefc566 100644 --- a/src/uu/ls/src/display.rs +++ b/src/uu/ls/src/display.rs @@ -752,14 +752,16 @@ fn display_item_name( let escaped_target = escape_name_with_locale(target_path.as_os_str(), config); // We get the absolute path to be able to construct PathData with valid Metadata. // This is because relative symlinks will fail to get_metadata. - let mut absolute_target = target_path.clone(); - if target_path.is_relative() { - if let Some(parent) = path.path().parent() { - absolute_target = parent.join(absolute_target); + let absolute_target = if target_path.is_relative() { + match path.path().parent() { + Some(p) => &p.join(&target_path), + None => &target_path, } - } + } else { + &target_path + }; - match fs::canonicalize(&absolute_target) { + match fs::canonicalize(absolute_target) { Ok(resolved_target) => { let target_data = PathData::new( resolved_target.as_path().into(), @@ -822,15 +824,15 @@ fn display_item_name( // to get correct alignment from later calls to`display_grid()`. if config.context { if let Some(pad_count) = prefix_context { - let security_context = if matches!(config.format, Format::Commas) { - path.security_context(config).to_string() + let security_context: Cow<'_, str> = if matches!(config.format, Format::Commas) { + path.security_context(config).into() } else { - pad_left(path.security_context(config), pad_count) + pad_left(path.security_context(config), pad_count).into() }; let old_name = name; name = OsString::with_capacity(security_context.len() + 1 + old_name.len()); - name.push(security_context); + name.push(security_context.as_ref()); name.push(" "); name.push(old_name); } From d96ba9eaa8e7cdb46b257d3d42e00608a6dfe7f2 Mon Sep 17 00:00:00 2001 From: "Guillem L. Jara" <4lon3ly0@tutanota.com> Date: Sun, 22 Mar 2026 16:24:57 +0100 Subject: [PATCH 040/264] fix(ls): apply reviewed changes Renames and documents PathDataDisplayName::Moo to _::Custom; also makes code more idiomatic and restores an accidentally-removed warning. Co-authored-by: Daniel Hofstetter Co-authored-by: xtqqczze <45661989+xtqqczze@users.noreply.github.com> --- src/uu/ls/src/config.rs | 20 ++++++-------------- src/uu/ls/src/ls.rs | 22 +++++++++++++++++----- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/src/uu/ls/src/config.rs b/src/uu/ls/src/config.rs index f775daafdb2..e2e797f1628 100644 --- a/src/uu/ls/src/config.rs +++ b/src/uu/ls/src/config.rs @@ -345,11 +345,6 @@ fn extract_time(options: &clap::ArgMatches) -> MetadataTimeField { fn is_color_compatible_term() -> bool { let term = std::env::var_os("TERM"); let colorterm = std::env::var_os("COLORTERM"); - let is_term_set = term.is_some(); - let is_colorterm_set = colorterm.is_some(); - - let term = term.unwrap_or_default(); - let colorterm = colorterm.unwrap_or_default(); // Search function in the TERM struct to manage the wildcards let term_matches = |term: &OsStr| -> bool { @@ -362,14 +357,11 @@ fn is_color_compatible_term() -> bool { }) }; - if is_term_set && term.is_empty() && is_colorterm_set && colorterm.is_empty() { - return false; - } - - if !term.is_empty() && !term_matches(&term) { - return false; + match (term, colorterm) { + (Some(t), Some(c)) if t.is_empty() && c.is_empty() => false, + (Some(t), _) if !t.is_empty() => term_matches(&t), + _ => true, } - true } /// Extracts the color option to use based on the options provided. @@ -1030,8 +1022,8 @@ fn parse_time_style(options: &clap::ArgMatches) -> Result<(String, Option(options::TIME_STYLE) - .map(|s| Cow::Borrowed(s.as_str())) - .or_else(|| std::env::var("TIME_STYLE").ok().map(Cow::Owned)) + .map(Cow::from) + .or_else(|| std::env::var("TIME_STYLE").ok().map(Cow::from)) { //If both FULL_TIME and TIME_STYLE are present //The one added last is dominant diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index bf6f5bcb66f..f689f5f6865 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -784,10 +784,12 @@ pub fn uu_app() -> Command { .after_help(translate!("ls-after-help")) } +/// Represents the possible values of [`PathData::display_name`]. The reason this is a +/// separate enum is to avoid a self-referential struct, as it is moved in hot loops. #[derive(Debug)] enum PathDataDisplayName<'a> { SelfReferential, - Moo(Cow<'a, OsStr>), + Custom(Cow<'a, OsStr>), } /// Represents a Path along with it's associated data. @@ -821,11 +823,11 @@ impl<'a> PathData<'a> { // We cannot use `Path::ends_with` or `Path::Components`, because they remove occurrences of '.' // For '..', the filename is None let display_name = if let Some(name) = file_name { - PathDataDisplayName::Moo(name) + PathDataDisplayName::Custom(name) } else if command_line { PathDataDisplayName::SelfReferential } else { - PathDataDisplayName::Moo( + PathDataDisplayName::Custom( dir_entry .as_ref() .map(DirEntry::file_name) @@ -952,7 +954,7 @@ impl<'a> PathData<'a> { fn display_name(&self) -> &OsStr { match self.display_name { PathDataDisplayName::SelfReferential => self.p_buf.as_os_str(), - PathDataDisplayName::Moo(ref cow) => cow, + PathDataDisplayName::Custom(ref cow) => cow, } } } @@ -1452,7 +1454,17 @@ fn get_security_context<'a>( let res: String = match str::from_utf8(context) { Ok(s) => s.to_string(), - Err(_) => String::from_utf8_lossy(context).into_owned(), + Err(e) => { + show_warning!( + "{}", + translate!( + "ls-warning-getting-security-context", + "path" => path.quote(), + "error" => e + ) + ); + String::from_utf8_lossy(context).into_owned() + } }; return Cow::Owned(res); From e4bae8eb20a67e7467586020e58927db8393f722 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 22 Mar 2026 09:44:42 +0100 Subject: [PATCH 041/264] uudoc: make tldr attribution and disclaimer translatable --- src/bin/uudoc.rs | 13 +++++-------- src/uucore/locales/en-US.ftl | 4 ++++ src/uucore/locales/fr-FR.ftl | 4 ++++ 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/bin/uudoc.rs b/src/bin/uudoc.rs index a2395d3aee7..64959bc12df 100644 --- a/src/bin/uudoc.rs +++ b/src/bin/uudoc.rs @@ -25,6 +25,7 @@ use zip::ZipArchive; use coreutils::validation; use uucore::Args; +use uucore::locale::get_message; include!(concat!(env!("OUT_DIR"), "/uutils_map.rs")); @@ -246,6 +247,8 @@ fn main() -> io::Result<()> { } } let utils = util_map::>>(); + // Initialize localization for uucore common strings (used by tldr example attribution) + let _ = uucore::locale::setup_localization("uudoc"); match std::fs::create_dir("docs/src/utils/") { Err(e) if e.kind() == io::ErrorKind::AlreadyExists => Ok(()), x => x, @@ -693,15 +696,9 @@ fn format_examples(content: String, output_markdown: bool) -> Result The examples are provided by the [tldr-pages project](https://tldr.sh) under the [CC BY 4.0 License](https://github.com/tldr-pages/tldr/blob/main/LICENSE.md)." - )?; + writeln!(s, "> {}", get_message("uudoc-tldr-attribution"))?; writeln!(s, ">")?; - writeln!( - s, - "> Please note that, as uutils is a work in progress, some examples might fail." - )?; + writeln!(s, "> {}", get_message("uudoc-tldr-disclaimer"))?; Ok(s) } diff --git a/src/uucore/locales/en-US.ftl b/src/uucore/locales/en-US.ftl index 15896209526..93a76157768 100644 --- a/src/uucore/locales/en-US.ftl +++ b/src/uucore/locales/en-US.ftl @@ -80,3 +80,7 @@ checksum-failed-open-file = { $count -> *[other] { $count } listed files could not be read } checksum-error-algo-bad-format = { $file }: { $line }: improperly formatted { $algo } checksum line + +# uudoc tldr examples messages +uudoc-tldr-attribution = The examples are provided by the [tldr-pages project](https://tldr.sh) under the [CC BY 4.0 License](https://github.com/tldr-pages/tldr/blob/main/LICENSE.md). +uudoc-tldr-disclaimer = Please note that, as uutils is a work in progress, some examples might fail. diff --git a/src/uucore/locales/fr-FR.ftl b/src/uucore/locales/fr-FR.ftl index e9ff4abe475..98a18fbce39 100644 --- a/src/uucore/locales/fr-FR.ftl +++ b/src/uucore/locales/fr-FR.ftl @@ -74,3 +74,7 @@ checksum-failed-open-file = { $count -> *[other] { $count } fichiers passés n'ont pas pu être lu } checksum-error-algo-bad-format = { $file }: { $line }: ligne invalide pour { $algo } + +# Messages uudoc pour les exemples tldr +uudoc-tldr-attribution = Les exemples sont fournis par le [projet tldr-pages](https://tldr.sh) sous la [licence CC BY 4.0](https://github.com/tldr-pages/tldr/blob/main/LICENSE.md). +uudoc-tldr-disclaimer = Veuillez noter que, uutils étant en cours de développement, certains exemples peuvent échouer. From c16f5d8ae6dbbcbba8494c7eb310679af89776e2 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Sat, 21 Mar 2026 19:03:59 +0900 Subject: [PATCH 042/264] GNUmakefile: Fix LN for cygwin --- GNUmakefile | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/GNUmakefile b/GNUmakefile index a99fefa46ec..7a00f4121c6 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -67,12 +67,10 @@ TOYBOX_SRC := $(TOYBOX_ROOT)/toybox-$(TOYBOX_VER) # todo: support building wasm OS := $(or $(CARGO_BUILD_TARGET),$(shell rustc --print host-tuple)) -# Windows does not allow symlink by default. -# Allow to override LN for AppArmor. -ifneq (,$(findstring windows,$(OS))) - LN ?= ln -f -endif -LN ?= ln -sf +# hardlinks are better default since +# - Windows(cygwin) does not allow symlink by default +# - std::env:current_exe resolves symlink +LN ?= ln -f SELINUX_PROGS := \ chcon \ From 8c51f2f62377e41a3425aeb4ec950ecb26f0b83d Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 22 Mar 2026 09:21:54 +0100 Subject: [PATCH 043/264] prepare release 0.8.0 --- Cargo.lock | 228 +++++++++++++++++++------------------- Cargo.toml | 230 +++++++++++++++++++-------------------- fuzz/Cargo.lock | 36 +++--- fuzz/uufuzz/Cargo.toml | 4 +- src/uu/stdbuf/Cargo.toml | 2 +- util/update-version.sh | 4 +- 6 files changed, 252 insertions(+), 252 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3c112f6da87..1a6a237c6e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -536,7 +536,7 @@ dependencies = [ [[package]] name = "coreutils" -version = "0.7.0" +version = "0.8.0" dependencies = [ "bytecount", "clap", @@ -3178,7 +3178,7 @@ dependencies = [ [[package]] name = "uu_arch" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3188,7 +3188,7 @@ dependencies = [ [[package]] name = "uu_b2sum" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3198,7 +3198,7 @@ dependencies = [ [[package]] name = "uu_base32" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3207,7 +3207,7 @@ dependencies = [ [[package]] name = "uu_base64" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3219,7 +3219,7 @@ dependencies = [ [[package]] name = "uu_basename" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3228,7 +3228,7 @@ dependencies = [ [[package]] name = "uu_basenc" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3238,7 +3238,7 @@ dependencies = [ [[package]] name = "uu_cat" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3253,7 +3253,7 @@ dependencies = [ [[package]] name = "uu_chcon" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3266,7 +3266,7 @@ dependencies = [ [[package]] name = "uu_checksum_common" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3275,7 +3275,7 @@ dependencies = [ [[package]] name = "uu_chgrp" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3284,7 +3284,7 @@ dependencies = [ [[package]] name = "uu_chmod" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3294,7 +3294,7 @@ dependencies = [ [[package]] name = "uu_chown" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3303,7 +3303,7 @@ dependencies = [ [[package]] name = "uu_chroot" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3313,7 +3313,7 @@ dependencies = [ [[package]] name = "uu_cksum" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3324,7 +3324,7 @@ dependencies = [ [[package]] name = "uu_comm" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3333,7 +3333,7 @@ dependencies = [ [[package]] name = "uu_cp" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3352,7 +3352,7 @@ dependencies = [ [[package]] name = "uu_csplit" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3365,7 +3365,7 @@ dependencies = [ [[package]] name = "uu_cut" -version = "0.7.0" +version = "0.8.0" dependencies = [ "bstr", "clap", @@ -3377,7 +3377,7 @@ dependencies = [ [[package]] name = "uu_date" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3396,7 +3396,7 @@ dependencies = [ [[package]] name = "uu_dd" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3411,7 +3411,7 @@ dependencies = [ [[package]] name = "uu_df" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3425,7 +3425,7 @@ dependencies = [ [[package]] name = "uu_dir" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "uu_ls", @@ -3434,7 +3434,7 @@ dependencies = [ [[package]] name = "uu_dircolors" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3443,7 +3443,7 @@ dependencies = [ [[package]] name = "uu_dirname" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3452,7 +3452,7 @@ dependencies = [ [[package]] name = "uu_du" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3467,7 +3467,7 @@ dependencies = [ [[package]] name = "uu_echo" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3476,7 +3476,7 @@ dependencies = [ [[package]] name = "uu_env" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3488,7 +3488,7 @@ dependencies = [ [[package]] name = "uu_expand" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3501,7 +3501,7 @@ dependencies = [ [[package]] name = "uu_expr" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3514,7 +3514,7 @@ dependencies = [ [[package]] name = "uu_factor" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3527,7 +3527,7 @@ dependencies = [ [[package]] name = "uu_false" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3537,7 +3537,7 @@ dependencies = [ [[package]] name = "uu_fmt" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3548,7 +3548,7 @@ dependencies = [ [[package]] name = "uu_fold" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3560,7 +3560,7 @@ dependencies = [ [[package]] name = "uu_groups" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3570,7 +3570,7 @@ dependencies = [ [[package]] name = "uu_head" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3581,7 +3581,7 @@ dependencies = [ [[package]] name = "uu_hostid" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3591,7 +3591,7 @@ dependencies = [ [[package]] name = "uu_hostname" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3605,7 +3605,7 @@ dependencies = [ [[package]] name = "uu_id" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3615,7 +3615,7 @@ dependencies = [ [[package]] name = "uu_install" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "file_diff", @@ -3628,7 +3628,7 @@ dependencies = [ [[package]] name = "uu_join" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3641,7 +3641,7 @@ dependencies = [ [[package]] name = "uu_kill" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3651,7 +3651,7 @@ dependencies = [ [[package]] name = "uu_link" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3660,7 +3660,7 @@ dependencies = [ [[package]] name = "uu_ln" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3670,7 +3670,7 @@ dependencies = [ [[package]] name = "uu_logname" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3680,7 +3680,7 @@ dependencies = [ [[package]] name = "uu_ls" -version = "0.7.0" +version = "0.8.0" dependencies = [ "ansi-width", "clap", @@ -3700,7 +3700,7 @@ dependencies = [ [[package]] name = "uu_md5sum" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3710,7 +3710,7 @@ dependencies = [ [[package]] name = "uu_mkdir" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3719,7 +3719,7 @@ dependencies = [ [[package]] name = "uu_mkfifo" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3729,7 +3729,7 @@ dependencies = [ [[package]] name = "uu_mknod" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3739,7 +3739,7 @@ dependencies = [ [[package]] name = "uu_mktemp" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3751,7 +3751,7 @@ dependencies = [ [[package]] name = "uu_more" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "crossterm", @@ -3762,7 +3762,7 @@ dependencies = [ [[package]] name = "uu_mv" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3779,7 +3779,7 @@ dependencies = [ [[package]] name = "uu_nice" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3790,7 +3790,7 @@ dependencies = [ [[package]] name = "uu_nl" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3803,7 +3803,7 @@ dependencies = [ [[package]] name = "uu_nohup" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3814,7 +3814,7 @@ dependencies = [ [[package]] name = "uu_nproc" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3824,7 +3824,7 @@ dependencies = [ [[package]] name = "uu_numfmt" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3836,7 +3836,7 @@ dependencies = [ [[package]] name = "uu_od" -version = "0.7.0" +version = "0.8.0" dependencies = [ "byteorder", "clap", @@ -3848,7 +3848,7 @@ dependencies = [ [[package]] name = "uu_paste" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3857,7 +3857,7 @@ dependencies = [ [[package]] name = "uu_pathchk" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3867,7 +3867,7 @@ dependencies = [ [[package]] name = "uu_pinky" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3876,7 +3876,7 @@ dependencies = [ [[package]] name = "uu_pr" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3889,7 +3889,7 @@ dependencies = [ [[package]] name = "uu_printenv" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3898,7 +3898,7 @@ dependencies = [ [[package]] name = "uu_printf" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3907,7 +3907,7 @@ dependencies = [ [[package]] name = "uu_ptx" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3918,7 +3918,7 @@ dependencies = [ [[package]] name = "uu_pwd" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3927,7 +3927,7 @@ dependencies = [ [[package]] name = "uu_readlink" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3936,7 +3936,7 @@ dependencies = [ [[package]] name = "uu_realpath" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3945,7 +3945,7 @@ dependencies = [ [[package]] name = "uu_rm" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3960,7 +3960,7 @@ dependencies = [ [[package]] name = "uu_rmdir" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3970,7 +3970,7 @@ dependencies = [ [[package]] name = "uu_runcon" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3982,7 +3982,7 @@ dependencies = [ [[package]] name = "uu_seq" -version = "0.7.0" +version = "0.8.0" dependencies = [ "bigdecimal", "clap", @@ -3996,7 +3996,7 @@ dependencies = [ [[package]] name = "uu_sha1sum" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -4006,7 +4006,7 @@ dependencies = [ [[package]] name = "uu_sha224sum" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -4016,7 +4016,7 @@ dependencies = [ [[package]] name = "uu_sha256sum" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -4026,7 +4026,7 @@ dependencies = [ [[package]] name = "uu_sha384sum" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -4036,7 +4036,7 @@ dependencies = [ [[package]] name = "uu_sha512sum" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -4046,7 +4046,7 @@ dependencies = [ [[package]] name = "uu_shred" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -4057,7 +4057,7 @@ dependencies = [ [[package]] name = "uu_shuf" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -4072,7 +4072,7 @@ dependencies = [ [[package]] name = "uu_sleep" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -4081,7 +4081,7 @@ dependencies = [ [[package]] name = "uu_sort" -version = "0.7.0" +version = "0.8.0" dependencies = [ "bigdecimal", "binary-heap-plus", @@ -4104,7 +4104,7 @@ dependencies = [ [[package]] name = "uu_split" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -4117,7 +4117,7 @@ dependencies = [ [[package]] name = "uu_stat" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -4127,7 +4127,7 @@ dependencies = [ [[package]] name = "uu_stdbuf" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -4139,7 +4139,7 @@ dependencies = [ [[package]] name = "uu_stdbuf_libstdbuf" -version = "0.7.0" +version = "0.8.0" dependencies = [ "ctor", "libc", @@ -4147,7 +4147,7 @@ dependencies = [ [[package]] name = "uu_stty" -version = "0.7.0" +version = "0.8.0" dependencies = [ "cfg_aliases", "clap", @@ -4158,7 +4158,7 @@ dependencies = [ [[package]] name = "uu_sum" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -4167,7 +4167,7 @@ dependencies = [ [[package]] name = "uu_sync" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -4178,7 +4178,7 @@ dependencies = [ [[package]] name = "uu_tac" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -4193,7 +4193,7 @@ dependencies = [ [[package]] name = "uu_tail" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -4209,7 +4209,7 @@ dependencies = [ [[package]] name = "uu_tee" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -4218,7 +4218,7 @@ dependencies = [ [[package]] name = "uu_test" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -4230,7 +4230,7 @@ dependencies = [ [[package]] name = "uu_timeout" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -4241,7 +4241,7 @@ dependencies = [ [[package]] name = "uu_touch" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "filetime", @@ -4257,7 +4257,7 @@ dependencies = [ [[package]] name = "uu_tr" -version = "0.7.0" +version = "0.8.0" dependencies = [ "bytecount", "clap", @@ -4268,7 +4268,7 @@ dependencies = [ [[package]] name = "uu_true" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -4278,7 +4278,7 @@ dependencies = [ [[package]] name = "uu_truncate" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -4287,7 +4287,7 @@ dependencies = [ [[package]] name = "uu_tsort" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -4301,7 +4301,7 @@ dependencies = [ [[package]] name = "uu_tty" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -4311,7 +4311,7 @@ dependencies = [ [[package]] name = "uu_uname" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -4321,7 +4321,7 @@ dependencies = [ [[package]] name = "uu_unexpand" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -4333,7 +4333,7 @@ dependencies = [ [[package]] name = "uu_uniq" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -4343,7 +4343,7 @@ dependencies = [ [[package]] name = "uu_unlink" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -4352,7 +4352,7 @@ dependencies = [ [[package]] name = "uu_uptime" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -4363,7 +4363,7 @@ dependencies = [ [[package]] name = "uu_users" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -4373,7 +4373,7 @@ dependencies = [ [[package]] name = "uu_vdir" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "uu_ls", @@ -4382,7 +4382,7 @@ dependencies = [ [[package]] name = "uu_wc" -version = "0.7.0" +version = "0.8.0" dependencies = [ "bytecount", "clap", @@ -4398,7 +4398,7 @@ dependencies = [ [[package]] name = "uu_who" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -4407,7 +4407,7 @@ dependencies = [ [[package]] name = "uu_whoami" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -4417,7 +4417,7 @@ dependencies = [ [[package]] name = "uu_yes" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -4427,7 +4427,7 @@ dependencies = [ [[package]] name = "uucore" -version = "0.7.0" +version = "0.8.0" dependencies = [ "base64-simd", "bigdecimal", @@ -4486,7 +4486,7 @@ dependencies = [ [[package]] name = "uucore_procs" -version = "0.7.0" +version = "0.8.0" dependencies = [ "proc-macro2", "quote", @@ -4504,7 +4504,7 @@ dependencies = [ [[package]] name = "uutests" -version = "0.7.0" +version = "0.8.0" dependencies = [ "ctor", "libc", diff --git a/Cargo.toml b/Cargo.toml index 9ef80cd072c..db9c0c40523 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -368,7 +368,7 @@ homepage = "https://github.com/uutils/coreutils" keywords = ["coreutils", "uutils", "cross-platform", "cli", "utility"] license = "MIT" readme = "README.package.md" -version = "0.7.0" +version = "0.8.0" [workspace.dependencies] ansi-width = "0.1.0" @@ -471,12 +471,12 @@ fluent-bundle = "0.16.0" unic-langid = "0.9.6" fluent-syntax = "0.12.0" -uucore = { version = "0.7.0", package = "uucore", path = "src/uucore" } -uucore_procs = { version = "0.7.0", package = "uucore_procs", path = "src/uucore_procs" } -uu_ls = { version = "0.7.0", path = "src/uu/ls" } -uu_base32 = { version = "0.7.0", path = "src/uu/base32" } -uu_checksum_common = { version = "0.7.0", path = "src/uu/checksum_common" } -uutests = { version = "0.7.0", package = "uutests", path = "tests/uutests" } +uucore = { version = "0.8.0", package = "uucore", path = "src/uucore" } +uucore_procs = { version = "0.8.0", package = "uucore_procs", path = "src/uucore_procs" } +uu_ls = { version = "0.8.0", path = "src/uu/ls" } +uu_base32 = { version = "0.8.0", path = "src/uu/base32" } +uu_checksum_common = { version = "0.8.0", path = "src/uu/checksum_common" } +uutests = { version = "0.8.0", package = "uutests", path = "tests/uutests" } [dependencies] clap.workspace = true @@ -494,115 +494,115 @@ zip = { workspace = true, optional = true } # * uutils -uu_test = { optional = true, version = "0.7.0", package = "uu_test", path = "src/uu/test" } +uu_test = { optional = true, version = "0.8.0", package = "uu_test", path = "src/uu/test" } # -arch = { optional = true, version = "0.7.0", package = "uu_arch", path = "src/uu/arch" } -base32 = { optional = true, version = "0.7.0", package = "uu_base32", path = "src/uu/base32" } -base64 = { optional = true, version = "0.7.0", package = "uu_base64", path = "src/uu/base64" } -basename = { optional = true, version = "0.7.0", package = "uu_basename", path = "src/uu/basename" } -basenc = { optional = true, version = "0.7.0", package = "uu_basenc", path = "src/uu/basenc" } -cat = { optional = true, version = "0.7.0", package = "uu_cat", path = "src/uu/cat" } -chcon = { optional = true, version = "0.7.0", package = "uu_chcon", path = "src/uu/chcon" } -chgrp = { optional = true, version = "0.7.0", package = "uu_chgrp", path = "src/uu/chgrp" } -chmod = { optional = true, version = "0.7.0", package = "uu_chmod", path = "src/uu/chmod" } -chown = { optional = true, version = "0.7.0", package = "uu_chown", path = "src/uu/chown" } -chroot = { optional = true, version = "0.7.0", package = "uu_chroot", path = "src/uu/chroot" } -cksum = { optional = true, version = "0.7.0", package = "uu_cksum", path = "src/uu/cksum" } -b2sum = { optional = true, version = "0.7.0", package = "uu_b2sum", path = "src/uu/b2sum" } -md5sum = { optional = true, version = "0.7.0", package = "uu_md5sum", path = "src/uu/md5sum" } -sha1sum = { optional = true, version = "0.7.0", package = "uu_sha1sum", path = "src/uu/sha1sum" } -sha224sum = { optional = true, version = "0.7.0", package = "uu_sha224sum", path = "src/uu/sha224sum" } -sha256sum = { optional = true, version = "0.7.0", package = "uu_sha256sum", path = "src/uu/sha256sum" } -sha384sum = { optional = true, version = "0.7.0", package = "uu_sha384sum", path = "src/uu/sha384sum" } -sha512sum = { optional = true, version = "0.7.0", package = "uu_sha512sum", path = "src/uu/sha512sum" } -comm = { optional = true, version = "0.7.0", package = "uu_comm", path = "src/uu/comm" } -cp = { optional = true, version = "0.7.0", package = "uu_cp", path = "src/uu/cp" } -csplit = { optional = true, version = "0.7.0", package = "uu_csplit", path = "src/uu/csplit" } -cut = { optional = true, version = "0.7.0", package = "uu_cut", path = "src/uu/cut" } -date = { optional = true, version = "0.7.0", package = "uu_date", path = "src/uu/date" } -dd = { optional = true, version = "0.7.0", package = "uu_dd", path = "src/uu/dd" } -df = { optional = true, version = "0.7.0", package = "uu_df", path = "src/uu/df" } -dir = { optional = true, version = "0.7.0", package = "uu_dir", path = "src/uu/dir" } -dircolors = { optional = true, version = "0.7.0", package = "uu_dircolors", path = "src/uu/dircolors" } -dirname = { optional = true, version = "0.7.0", package = "uu_dirname", path = "src/uu/dirname" } -du = { optional = true, version = "0.7.0", package = "uu_du", path = "src/uu/du" } -echo = { optional = true, version = "0.7.0", package = "uu_echo", path = "src/uu/echo" } -env = { optional = true, version = "0.7.0", package = "uu_env", path = "src/uu/env" } -expand = { optional = true, version = "0.7.0", package = "uu_expand", path = "src/uu/expand" } -expr = { optional = true, version = "0.7.0", package = "uu_expr", path = "src/uu/expr" } -factor = { optional = true, version = "0.7.0", package = "uu_factor", path = "src/uu/factor" } -false = { optional = true, version = "0.7.0", package = "uu_false", path = "src/uu/false" } -fmt = { optional = true, version = "0.7.0", package = "uu_fmt", path = "src/uu/fmt" } -fold = { optional = true, version = "0.7.0", package = "uu_fold", path = "src/uu/fold" } -groups = { optional = true, version = "0.7.0", package = "uu_groups", path = "src/uu/groups" } -head = { optional = true, version = "0.7.0", package = "uu_head", path = "src/uu/head" } -hostid = { optional = true, version = "0.7.0", package = "uu_hostid", path = "src/uu/hostid" } -hostname = { optional = true, version = "0.7.0", package = "uu_hostname", path = "src/uu/hostname" } -id = { optional = true, version = "0.7.0", package = "uu_id", path = "src/uu/id" } -install = { optional = true, version = "0.7.0", package = "uu_install", path = "src/uu/install" } -join = { optional = true, version = "0.7.0", package = "uu_join", path = "src/uu/join" } -kill = { optional = true, version = "0.7.0", package = "uu_kill", path = "src/uu/kill" } -link = { optional = true, version = "0.7.0", package = "uu_link", path = "src/uu/link" } -ln = { optional = true, version = "0.7.0", package = "uu_ln", path = "src/uu/ln" } -ls = { optional = true, version = "0.7.0", package = "uu_ls", path = "src/uu/ls" } -logname = { optional = true, version = "0.7.0", package = "uu_logname", path = "src/uu/logname" } -mkdir = { optional = true, version = "0.7.0", package = "uu_mkdir", path = "src/uu/mkdir" } -mkfifo = { optional = true, version = "0.7.0", package = "uu_mkfifo", path = "src/uu/mkfifo" } -mknod = { optional = true, version = "0.7.0", package = "uu_mknod", path = "src/uu/mknod" } -mktemp = { optional = true, version = "0.7.0", package = "uu_mktemp", path = "src/uu/mktemp" } -more = { optional = true, version = "0.7.0", package = "uu_more", path = "src/uu/more" } -mv = { optional = true, version = "0.7.0", package = "uu_mv", path = "src/uu/mv" } -nice = { optional = true, version = "0.7.0", package = "uu_nice", path = "src/uu/nice" } -nl = { optional = true, version = "0.7.0", package = "uu_nl", path = "src/uu/nl" } -nohup = { optional = true, version = "0.7.0", package = "uu_nohup", path = "src/uu/nohup" } -nproc = { optional = true, version = "0.7.0", package = "uu_nproc", path = "src/uu/nproc" } -numfmt = { optional = true, version = "0.7.0", package = "uu_numfmt", path = "src/uu/numfmt" } -od = { optional = true, version = "0.7.0", package = "uu_od", path = "src/uu/od" } -paste = { optional = true, version = "0.7.0", package = "uu_paste", path = "src/uu/paste" } -pathchk = { optional = true, version = "0.7.0", package = "uu_pathchk", path = "src/uu/pathchk" } -pinky = { optional = true, version = "0.7.0", package = "uu_pinky", path = "src/uu/pinky" } -pr = { optional = true, version = "0.7.0", package = "uu_pr", path = "src/uu/pr" } -printenv = { optional = true, version = "0.7.0", package = "uu_printenv", path = "src/uu/printenv" } -printf = { optional = true, version = "0.7.0", package = "uu_printf", path = "src/uu/printf" } -ptx = { optional = true, version = "0.7.0", package = "uu_ptx", path = "src/uu/ptx" } -pwd = { optional = true, version = "0.7.0", package = "uu_pwd", path = "src/uu/pwd" } -readlink = { optional = true, version = "0.7.0", package = "uu_readlink", path = "src/uu/readlink" } -realpath = { optional = true, version = "0.7.0", package = "uu_realpath", path = "src/uu/realpath" } -rm = { optional = true, version = "0.7.0", package = "uu_rm", path = "src/uu/rm" } -rmdir = { optional = true, version = "0.7.0", package = "uu_rmdir", path = "src/uu/rmdir" } -runcon = { optional = true, version = "0.7.0", package = "uu_runcon", path = "src/uu/runcon" } -seq = { optional = true, version = "0.7.0", package = "uu_seq", path = "src/uu/seq" } -shred = { optional = true, version = "0.7.0", package = "uu_shred", path = "src/uu/shred" } -shuf = { optional = true, version = "0.7.0", package = "uu_shuf", path = "src/uu/shuf" } -sleep = { optional = true, version = "0.7.0", package = "uu_sleep", path = "src/uu/sleep" } -sort = { optional = true, version = "0.7.0", package = "uu_sort", path = "src/uu/sort" } -split = { optional = true, version = "0.7.0", package = "uu_split", path = "src/uu/split" } -stat = { optional = true, version = "0.7.0", package = "uu_stat", path = "src/uu/stat" } -stdbuf = { optional = true, version = "0.7.0", package = "uu_stdbuf", path = "src/uu/stdbuf" } -stty = { optional = true, version = "0.7.0", package = "uu_stty", path = "src/uu/stty" } -sum = { optional = true, version = "0.7.0", package = "uu_sum", path = "src/uu/sum" } -sync = { optional = true, version = "0.7.0", package = "uu_sync", path = "src/uu/sync" } -tac = { optional = true, version = "0.7.0", package = "uu_tac", path = "src/uu/tac" } -tail = { optional = true, version = "0.7.0", package = "uu_tail", path = "src/uu/tail" } -tee = { optional = true, version = "0.7.0", package = "uu_tee", path = "src/uu/tee" } -timeout = { optional = true, version = "0.7.0", package = "uu_timeout", path = "src/uu/timeout" } -touch = { optional = true, version = "0.7.0", package = "uu_touch", path = "src/uu/touch" } -tr = { optional = true, version = "0.7.0", package = "uu_tr", path = "src/uu/tr" } -true = { optional = true, version = "0.7.0", package = "uu_true", path = "src/uu/true" } -truncate = { optional = true, version = "0.7.0", package = "uu_truncate", path = "src/uu/truncate" } -tsort = { optional = true, version = "0.7.0", package = "uu_tsort", path = "src/uu/tsort" } -tty = { optional = true, version = "0.7.0", package = "uu_tty", path = "src/uu/tty" } -uname = { optional = true, version = "0.7.0", package = "uu_uname", path = "src/uu/uname" } -unexpand = { optional = true, version = "0.7.0", package = "uu_unexpand", path = "src/uu/unexpand" } -uniq = { optional = true, version = "0.7.0", package = "uu_uniq", path = "src/uu/uniq" } -unlink = { optional = true, version = "0.7.0", package = "uu_unlink", path = "src/uu/unlink" } -uptime = { optional = true, version = "0.7.0", package = "uu_uptime", path = "src/uu/uptime" } -users = { optional = true, version = "0.7.0", package = "uu_users", path = "src/uu/users" } -vdir = { optional = true, version = "0.7.0", package = "uu_vdir", path = "src/uu/vdir" } -wc = { optional = true, version = "0.7.0", package = "uu_wc", path = "src/uu/wc" } -who = { optional = true, version = "0.7.0", package = "uu_who", path = "src/uu/who" } -whoami = { optional = true, version = "0.7.0", package = "uu_whoami", path = "src/uu/whoami" } -yes = { optional = true, version = "0.7.0", package = "uu_yes", path = "src/uu/yes" } +arch = { optional = true, version = "0.8.0", package = "uu_arch", path = "src/uu/arch" } +base32 = { optional = true, version = "0.8.0", package = "uu_base32", path = "src/uu/base32" } +base64 = { optional = true, version = "0.8.0", package = "uu_base64", path = "src/uu/base64" } +basename = { optional = true, version = "0.8.0", package = "uu_basename", path = "src/uu/basename" } +basenc = { optional = true, version = "0.8.0", package = "uu_basenc", path = "src/uu/basenc" } +cat = { optional = true, version = "0.8.0", package = "uu_cat", path = "src/uu/cat" } +chcon = { optional = true, version = "0.8.0", package = "uu_chcon", path = "src/uu/chcon" } +chgrp = { optional = true, version = "0.8.0", package = "uu_chgrp", path = "src/uu/chgrp" } +chmod = { optional = true, version = "0.8.0", package = "uu_chmod", path = "src/uu/chmod" } +chown = { optional = true, version = "0.8.0", package = "uu_chown", path = "src/uu/chown" } +chroot = { optional = true, version = "0.8.0", package = "uu_chroot", path = "src/uu/chroot" } +cksum = { optional = true, version = "0.8.0", package = "uu_cksum", path = "src/uu/cksum" } +b2sum = { optional = true, version = "0.8.0", package = "uu_b2sum", path = "src/uu/b2sum" } +md5sum = { optional = true, version = "0.8.0", package = "uu_md5sum", path = "src/uu/md5sum" } +sha1sum = { optional = true, version = "0.8.0", package = "uu_sha1sum", path = "src/uu/sha1sum" } +sha224sum = { optional = true, version = "0.8.0", package = "uu_sha224sum", path = "src/uu/sha224sum" } +sha256sum = { optional = true, version = "0.8.0", package = "uu_sha256sum", path = "src/uu/sha256sum" } +sha384sum = { optional = true, version = "0.8.0", package = "uu_sha384sum", path = "src/uu/sha384sum" } +sha512sum = { optional = true, version = "0.8.0", package = "uu_sha512sum", path = "src/uu/sha512sum" } +comm = { optional = true, version = "0.8.0", package = "uu_comm", path = "src/uu/comm" } +cp = { optional = true, version = "0.8.0", package = "uu_cp", path = "src/uu/cp" } +csplit = { optional = true, version = "0.8.0", package = "uu_csplit", path = "src/uu/csplit" } +cut = { optional = true, version = "0.8.0", package = "uu_cut", path = "src/uu/cut" } +date = { optional = true, version = "0.8.0", package = "uu_date", path = "src/uu/date" } +dd = { optional = true, version = "0.8.0", package = "uu_dd", path = "src/uu/dd" } +df = { optional = true, version = "0.8.0", package = "uu_df", path = "src/uu/df" } +dir = { optional = true, version = "0.8.0", package = "uu_dir", path = "src/uu/dir" } +dircolors = { optional = true, version = "0.8.0", package = "uu_dircolors", path = "src/uu/dircolors" } +dirname = { optional = true, version = "0.8.0", package = "uu_dirname", path = "src/uu/dirname" } +du = { optional = true, version = "0.8.0", package = "uu_du", path = "src/uu/du" } +echo = { optional = true, version = "0.8.0", package = "uu_echo", path = "src/uu/echo" } +env = { optional = true, version = "0.8.0", package = "uu_env", path = "src/uu/env" } +expand = { optional = true, version = "0.8.0", package = "uu_expand", path = "src/uu/expand" } +expr = { optional = true, version = "0.8.0", package = "uu_expr", path = "src/uu/expr" } +factor = { optional = true, version = "0.8.0", package = "uu_factor", path = "src/uu/factor" } +false = { optional = true, version = "0.8.0", package = "uu_false", path = "src/uu/false" } +fmt = { optional = true, version = "0.8.0", package = "uu_fmt", path = "src/uu/fmt" } +fold = { optional = true, version = "0.8.0", package = "uu_fold", path = "src/uu/fold" } +groups = { optional = true, version = "0.8.0", package = "uu_groups", path = "src/uu/groups" } +head = { optional = true, version = "0.8.0", package = "uu_head", path = "src/uu/head" } +hostid = { optional = true, version = "0.8.0", package = "uu_hostid", path = "src/uu/hostid" } +hostname = { optional = true, version = "0.8.0", package = "uu_hostname", path = "src/uu/hostname" } +id = { optional = true, version = "0.8.0", package = "uu_id", path = "src/uu/id" } +install = { optional = true, version = "0.8.0", package = "uu_install", path = "src/uu/install" } +join = { optional = true, version = "0.8.0", package = "uu_join", path = "src/uu/join" } +kill = { optional = true, version = "0.8.0", package = "uu_kill", path = "src/uu/kill" } +link = { optional = true, version = "0.8.0", package = "uu_link", path = "src/uu/link" } +ln = { optional = true, version = "0.8.0", package = "uu_ln", path = "src/uu/ln" } +ls = { optional = true, version = "0.8.0", package = "uu_ls", path = "src/uu/ls" } +logname = { optional = true, version = "0.8.0", package = "uu_logname", path = "src/uu/logname" } +mkdir = { optional = true, version = "0.8.0", package = "uu_mkdir", path = "src/uu/mkdir" } +mkfifo = { optional = true, version = "0.8.0", package = "uu_mkfifo", path = "src/uu/mkfifo" } +mknod = { optional = true, version = "0.8.0", package = "uu_mknod", path = "src/uu/mknod" } +mktemp = { optional = true, version = "0.8.0", package = "uu_mktemp", path = "src/uu/mktemp" } +more = { optional = true, version = "0.8.0", package = "uu_more", path = "src/uu/more" } +mv = { optional = true, version = "0.8.0", package = "uu_mv", path = "src/uu/mv" } +nice = { optional = true, version = "0.8.0", package = "uu_nice", path = "src/uu/nice" } +nl = { optional = true, version = "0.8.0", package = "uu_nl", path = "src/uu/nl" } +nohup = { optional = true, version = "0.8.0", package = "uu_nohup", path = "src/uu/nohup" } +nproc = { optional = true, version = "0.8.0", package = "uu_nproc", path = "src/uu/nproc" } +numfmt = { optional = true, version = "0.8.0", package = "uu_numfmt", path = "src/uu/numfmt" } +od = { optional = true, version = "0.8.0", package = "uu_od", path = "src/uu/od" } +paste = { optional = true, version = "0.8.0", package = "uu_paste", path = "src/uu/paste" } +pathchk = { optional = true, version = "0.8.0", package = "uu_pathchk", path = "src/uu/pathchk" } +pinky = { optional = true, version = "0.8.0", package = "uu_pinky", path = "src/uu/pinky" } +pr = { optional = true, version = "0.8.0", package = "uu_pr", path = "src/uu/pr" } +printenv = { optional = true, version = "0.8.0", package = "uu_printenv", path = "src/uu/printenv" } +printf = { optional = true, version = "0.8.0", package = "uu_printf", path = "src/uu/printf" } +ptx = { optional = true, version = "0.8.0", package = "uu_ptx", path = "src/uu/ptx" } +pwd = { optional = true, version = "0.8.0", package = "uu_pwd", path = "src/uu/pwd" } +readlink = { optional = true, version = "0.8.0", package = "uu_readlink", path = "src/uu/readlink" } +realpath = { optional = true, version = "0.8.0", package = "uu_realpath", path = "src/uu/realpath" } +rm = { optional = true, version = "0.8.0", package = "uu_rm", path = "src/uu/rm" } +rmdir = { optional = true, version = "0.8.0", package = "uu_rmdir", path = "src/uu/rmdir" } +runcon = { optional = true, version = "0.8.0", package = "uu_runcon", path = "src/uu/runcon" } +seq = { optional = true, version = "0.8.0", package = "uu_seq", path = "src/uu/seq" } +shred = { optional = true, version = "0.8.0", package = "uu_shred", path = "src/uu/shred" } +shuf = { optional = true, version = "0.8.0", package = "uu_shuf", path = "src/uu/shuf" } +sleep = { optional = true, version = "0.8.0", package = "uu_sleep", path = "src/uu/sleep" } +sort = { optional = true, version = "0.8.0", package = "uu_sort", path = "src/uu/sort" } +split = { optional = true, version = "0.8.0", package = "uu_split", path = "src/uu/split" } +stat = { optional = true, version = "0.8.0", package = "uu_stat", path = "src/uu/stat" } +stdbuf = { optional = true, version = "0.8.0", package = "uu_stdbuf", path = "src/uu/stdbuf" } +stty = { optional = true, version = "0.8.0", package = "uu_stty", path = "src/uu/stty" } +sum = { optional = true, version = "0.8.0", package = "uu_sum", path = "src/uu/sum" } +sync = { optional = true, version = "0.8.0", package = "uu_sync", path = "src/uu/sync" } +tac = { optional = true, version = "0.8.0", package = "uu_tac", path = "src/uu/tac" } +tail = { optional = true, version = "0.8.0", package = "uu_tail", path = "src/uu/tail" } +tee = { optional = true, version = "0.8.0", package = "uu_tee", path = "src/uu/tee" } +timeout = { optional = true, version = "0.8.0", package = "uu_timeout", path = "src/uu/timeout" } +touch = { optional = true, version = "0.8.0", package = "uu_touch", path = "src/uu/touch" } +tr = { optional = true, version = "0.8.0", package = "uu_tr", path = "src/uu/tr" } +true = { optional = true, version = "0.8.0", package = "uu_true", path = "src/uu/true" } +truncate = { optional = true, version = "0.8.0", package = "uu_truncate", path = "src/uu/truncate" } +tsort = { optional = true, version = "0.8.0", package = "uu_tsort", path = "src/uu/tsort" } +tty = { optional = true, version = "0.8.0", package = "uu_tty", path = "src/uu/tty" } +uname = { optional = true, version = "0.8.0", package = "uu_uname", path = "src/uu/uname" } +unexpand = { optional = true, version = "0.8.0", package = "uu_unexpand", path = "src/uu/unexpand" } +uniq = { optional = true, version = "0.8.0", package = "uu_uniq", path = "src/uu/uniq" } +unlink = { optional = true, version = "0.8.0", package = "uu_unlink", path = "src/uu/unlink" } +uptime = { optional = true, version = "0.8.0", package = "uu_uptime", path = "src/uu/uptime" } +users = { optional = true, version = "0.8.0", package = "uu_users", path = "src/uu/users" } +vdir = { optional = true, version = "0.8.0", package = "uu_vdir", path = "src/uu/vdir" } +wc = { optional = true, version = "0.8.0", package = "uu_wc", path = "src/uu/wc" } +who = { optional = true, version = "0.8.0", package = "uu_who", path = "src/uu/who" } +whoami = { optional = true, version = "0.8.0", package = "uu_whoami", path = "src/uu/whoami" } +yes = { optional = true, version = "0.8.0", package = "uu_yes", path = "src/uu/yes" } # this breaks clippy linting with: "tests/by-util/test_factor_benches.rs: No such file or directory (os error 2)" # factor_benches = { optional = true, version = "0.0.0", package = "uu_factor_benches", path = "tests/benches/factor" } diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index 35e7ed653b9..9363e1f46ce 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -1885,7 +1885,7 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uu_checksum_common" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -1894,7 +1894,7 @@ dependencies = [ [[package]] name = "uu_cksum" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -1904,7 +1904,7 @@ dependencies = [ [[package]] name = "uu_cut" -version = "0.7.0" +version = "0.8.0" dependencies = [ "bstr", "clap", @@ -1915,7 +1915,7 @@ dependencies = [ [[package]] name = "uu_date" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -1932,7 +1932,7 @@ dependencies = [ [[package]] name = "uu_dirname" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -1941,7 +1941,7 @@ dependencies = [ [[package]] name = "uu_echo" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -1950,7 +1950,7 @@ dependencies = [ [[package]] name = "uu_env" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -1962,7 +1962,7 @@ dependencies = [ [[package]] name = "uu_expr" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -1975,7 +1975,7 @@ dependencies = [ [[package]] name = "uu_printf" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -1984,7 +1984,7 @@ dependencies = [ [[package]] name = "uu_seq" -version = "0.7.0" +version = "0.8.0" dependencies = [ "bigdecimal", "clap", @@ -1997,7 +1997,7 @@ dependencies = [ [[package]] name = "uu_sort" -version = "0.7.0" +version = "0.8.0" dependencies = [ "bigdecimal", "binary-heap-plus", @@ -2019,7 +2019,7 @@ dependencies = [ [[package]] name = "uu_split" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -2030,7 +2030,7 @@ dependencies = [ [[package]] name = "uu_test" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -2041,7 +2041,7 @@ dependencies = [ [[package]] name = "uu_tr" -version = "0.7.0" +version = "0.8.0" dependencies = [ "bytecount", "clap", @@ -2052,7 +2052,7 @@ dependencies = [ [[package]] name = "uu_wc" -version = "0.7.0" +version = "0.8.0" dependencies = [ "bytecount", "clap", @@ -2066,7 +2066,7 @@ dependencies = [ [[package]] name = "uucore" -version = "0.7.0" +version = "0.8.0" dependencies = [ "base64-simd", "bigdecimal", @@ -2140,7 +2140,7 @@ dependencies = [ [[package]] name = "uucore_procs" -version = "0.7.0" +version = "0.8.0" dependencies = [ "proc-macro2", "quote", @@ -2148,7 +2148,7 @@ dependencies = [ [[package]] name = "uufuzz" -version = "0.7.0" +version = "0.8.0" dependencies = [ "console", "libc", diff --git a/fuzz/uufuzz/Cargo.toml b/fuzz/uufuzz/Cargo.toml index 4480061763c..39f24fd20c5 100644 --- a/fuzz/uufuzz/Cargo.toml +++ b/fuzz/uufuzz/Cargo.toml @@ -3,7 +3,7 @@ name = "uufuzz" authors = ["uutils developers"] description = "uutils ~ 'core' uutils fuzzing library" repository = "https://github.com/uutils/coreutils/tree/main/fuzz/uufuzz" -version = "0.7.0" +version = "0.8.0" edition.workspace = true rust-version.workspace = true license.workspace = true @@ -13,5 +13,5 @@ console = "0.16.0" libc = "0.2.153" rand = { version = "0.9.0", features = ["small_rng"] } similar = "2.5.0" -uucore = { version = "0.7.0", path = "../../src/uucore", features = ["parser"] } +uucore = { version = "0.8.0", path = "../../src/uucore", features = ["parser"] } tempfile = "3.15.0" diff --git a/src/uu/stdbuf/Cargo.toml b/src/uu/stdbuf/Cargo.toml index da02c726c1b..7510be1a4f4 100644 --- a/src/uu/stdbuf/Cargo.toml +++ b/src/uu/stdbuf/Cargo.toml @@ -21,7 +21,7 @@ path = "src/stdbuf.rs" [dependencies] clap = { workspace = true } -libstdbuf = { package = "uu_stdbuf_libstdbuf", version = "0.7.0", path = "src/libstdbuf" } +libstdbuf = { package = "uu_stdbuf_libstdbuf", version = "0.8.0", path = "src/libstdbuf" } tempfile = { workspace = true } uucore = { workspace = true, features = ["parser-size"] } thiserror = { workspace = true } diff --git a/util/update-version.sh b/util/update-version.sh index 1290cc213b1..5e2f507259f 100755 --- a/util/update-version.sh +++ b/util/update-version.sh @@ -17,8 +17,8 @@ # 10) Create the release on github https://github.com/uutils/coreutils/releases/new # 11) Make sure we have good release notes -FROM="0.6.0" -TO="0.7.0" +FROM="0.7.0" +TO="0.8.0" PROGS=$(ls -1d src/uu/*/Cargo.toml src/uu/stdbuf/src/libstdbuf/Cargo.toml src/uucore/Cargo.toml Cargo.toml fuzz/uufuzz/Cargo.toml src/uu/stdbuf/Cargo.toml) From 577a158b356cf71c969ca457c03fc2a9365a641d Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 22 Mar 2026 16:56:00 +0100 Subject: [PATCH 044/264] update-version.sh: update Cargo.lock files after version bump --- util/update-version.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/util/update-version.sh b/util/update-version.sh index 5e2f507259f..d80467e22b7 100755 --- a/util/update-version.sh +++ b/util/update-version.sh @@ -48,3 +48,7 @@ sed -i -e "s|uucore = { version=\">=$FROM\",|uucore = { version=\">=$TO\",|" $PR # Update crates using uucore_procs #shellcheck disable=SC2086 sed -i -e "s|uucore_procs = { version=\">=$FROM\",|uucore_procs = { version=\">=$TO\",|" $PROGS + +# Update Cargo.lock files +cargo update --workspace +cargo update --workspace --manifest-path fuzz/Cargo.toml From 8816416dc2ea32f3818bc9f5f175ce805c820cd4 Mon Sep 17 00:00:00 2001 From: pin Date: Fri, 20 Mar 2026 09:43:50 +0100 Subject: [PATCH 045/264] Fix `stdbuf` on NetBSD closes #11410 --- src/uu/stdbuf/src/libstdbuf/src/libstdbuf.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/uu/stdbuf/src/libstdbuf/src/libstdbuf.rs b/src/uu/stdbuf/src/libstdbuf/src/libstdbuf.rs index 26d8a9829d5..f675efca86c 100644 --- a/src/uu/stdbuf/src/libstdbuf/src/libstdbuf.rs +++ b/src/uu/stdbuf/src/libstdbuf/src/libstdbuf.rs @@ -35,6 +35,11 @@ pub unsafe extern "C" fn __stdbuf_get_stdin() -> *mut FILE { unsafe { __stdin } } + #[cfg(target_os = "netbsd")] + { + unsafe { libc::fdopen(0, c"r".as_ptr()) } + } + #[cfg(target_os = "cygwin")] { // _getreent()->_std{in,out,err} @@ -61,6 +66,7 @@ pub unsafe extern "C" fn __stdbuf_get_stdin() -> *mut FILE { #[cfg(not(any( target_os = "macos", target_os = "freebsd", + target_os = "netbsd", target_os = "openbsd", target_os = "cygwin" )))] @@ -92,6 +98,11 @@ pub unsafe extern "C" fn __stdbuf_get_stdout() -> *mut FILE { unsafe { __stdout } } + #[cfg(target_os = "netbsd")] + { + unsafe { libc::fdopen(1, c"w".as_ptr()) } + } + #[cfg(target_os = "cygwin")] { // _getreent()->_std{in,out,err} @@ -118,6 +129,7 @@ pub unsafe extern "C" fn __stdbuf_get_stdout() -> *mut FILE { #[cfg(not(any( target_os = "macos", target_os = "freebsd", + target_os = "netbsd", target_os = "openbsd", target_os = "cygwin" )))] @@ -149,6 +161,11 @@ pub unsafe extern "C" fn __stdbuf_get_stderr() -> *mut FILE { unsafe { __stderr } } + #[cfg(target_os = "netbsd")] + { + unsafe { libc::fdopen(2, c"w".as_ptr()) } + } + #[cfg(target_os = "cygwin")] { // _getreent()->_std{in,out,err} @@ -175,6 +192,7 @@ pub unsafe extern "C" fn __stdbuf_get_stderr() -> *mut FILE { #[cfg(not(any( target_os = "macos", target_os = "freebsd", + target_os = "netbsd", target_os = "openbsd", target_os = "cygwin" )))] From 4a344f43755832c7997c1501dc0ebd9b93c3fc78 Mon Sep 17 00:00:00 2001 From: pin Date: Fri, 20 Mar 2026 09:48:34 +0100 Subject: [PATCH 046/264] silence apell checker --- src/uu/stdbuf/src/libstdbuf/src/libstdbuf.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uu/stdbuf/src/libstdbuf/src/libstdbuf.rs b/src/uu/stdbuf/src/libstdbuf/src/libstdbuf.rs index f675efca86c..deb8ce7ff4a 100644 --- a/src/uu/stdbuf/src/libstdbuf/src/libstdbuf.rs +++ b/src/uu/stdbuf/src/libstdbuf/src/libstdbuf.rs @@ -2,7 +2,7 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (ToDO) getreent reent IOFBF IOLBF IONBF setvbuf stderrp stdinp stdoutp +// spell-checker:ignore (ToDO) getreent reent IOFBF IOLBF IONBF setvbuf stderrp stdinp stdoutp fdopen use ctor::ctor; use libc::{_IOFBF, _IOLBF, _IONBF, FILE, c_char, c_int, fileno, size_t}; From 5726b02b4982f3adf5237197506311d44e5609c0 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Sun, 22 Mar 2026 03:28:28 +0900 Subject: [PATCH 047/264] coreutils: --help >/dev/full panics --- src/bin/coreutils.rs | 63 ++++++++++++++++++++++++++++---------------- 1 file changed, 41 insertions(+), 22 deletions(-) diff --git a/src/bin/coreutils.rs b/src/bin/coreutils.rs index 59634849a6b..383115baa82 100644 --- a/src/bin/coreutils.rs +++ b/src/bin/coreutils.rs @@ -17,31 +17,40 @@ const VERSION: &str = env!("CARGO_PKG_VERSION"); include!(concat!(env!("OUT_DIR"), "/uutils_map.rs")); fn usage(utils: &UtilityMap, name: &str) { - println!("{name} {VERSION} (multi-call binary)\n"); - println!("Usage: {name} [function [arguments...]]"); - println!(" {name} --list"); - println!(); + let display_list = utils.keys().copied().join(", "); + let width = cmp::min(textwrap::termwidth(), 100) - 8; // (opinion/heuristic) max 100 chars wide with 4 character side indentions + let indent_list = textwrap::indent(&textwrap::fill(&display_list, width), " "); #[cfg(feature = "feat_common_core")] + let common_core_string = " +Functions: + '' [arguments...] + +"; + #[cfg(not(feature = "feat_common_core"))] + let common_core_string = ""; + let s = format!( + "{name} {VERSION} (multi-call binary) + +Usage: {name} [function [arguments...]] + {name} --list + +{common_core_string}Options: + --list lists all defined functions, one per row + +Currently defined functions: + +{indent_list}" + ); + if let Err(e) = writeln!(io::stdout(), "{s}") + && e.kind() != io::ErrorKind::BrokenPipe { - println!("Functions:"); - println!(" '' [arguments...]"); - println!(); + let _ = writeln!(io::stderr(), "coreutils: {e}"); + process::exit(1); } - println!("Options:"); - println!(" --list lists all defined functions, one per row\n"); - println!("Currently defined functions:\n"); - let display_list = utils.keys().copied().join(", "); - let width = cmp::min(textwrap::termwidth(), 100) - 4 * 2; // (opinion/heuristic) max 100 chars wide with 4 character side indentions - println!( - "{}", - textwrap::indent(&textwrap::fill(&display_list, width), " ") - ); } #[allow(clippy::cognitive_complexity)] fn main() { - uucore::panic::mute_sigpipe_panic(); - let utils = util_map(); let mut args = uucore::args_os(); @@ -81,14 +90,24 @@ fn main() { usage(&utils, binary_as_util); process::exit(0); } - let utils: Vec<_> = utils.keys().collect(); - for util in utils { - println!("{util}"); + let mut out = io::stdout().lock(); + for util in utils.keys() { + if let Err(e) = writeln!(out, "{util}") + && e.kind() != io::ErrorKind::BrokenPipe + { + let _ = writeln!(io::stderr(), "coreutils: {e}"); + process::exit(1); + } } process::exit(0); } "--version" | "-V" => { - println!("{binary_as_util} {VERSION} (multi-call binary)"); + if let Err(e) = writeln!(io::stdout(), "coreutils {VERSION} (multi-call binary)") + && e.kind() != io::ErrorKind::BrokenPipe + { + let _ = writeln!(io::stderr(), "coreutils: {e}"); + process::exit(1); + } process::exit(0); } // Not a special command: fallthrough to calling a util From 2f07ebe203dc50c1a22f4eab6b3f407537aa82b5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 00:01:13 +0000 Subject: [PATCH 048/264] chore(deps): update endbug/add-and-commit action to v10 --- .github/workflows/FixPR.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/FixPR.yml b/.github/workflows/FixPR.yml index d086687e86d..0024aee5883 100644 --- a/.github/workflows/FixPR.yml +++ b/.github/workflows/FixPR.yml @@ -69,7 +69,7 @@ jobs: ## * using the 'stable' toolchain is necessary to avoid "unexpected '--filter-platform'" errors cargo +stable tree --locked --no-dedupe -e=no-dev --prefix=none --features ${{ matrix.job.features }} | grep -vE "$PWD" | sort --unique - name: Commit any changes (to '${{ env.BRANCH_TARGET }}') - uses: EndBug/add-and-commit@v9 + uses: EndBug/add-and-commit@v10 with: new_branch: ${{ env.BRANCH_TARGET }} default_author: github_actions From 05ce917b3aa354cf8f544e76102d02e3c6855721 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 22 Mar 2026 22:19:14 +0100 Subject: [PATCH 049/264] ls: simplify canonicalize_indicator_value --- src/uu/ls/src/colors.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uu/ls/src/colors.rs b/src/uu/ls/src/colors.rs index 66dd4f6a2ef..a50418cd987 100644 --- a/src/uu/ls/src/colors.rs +++ b/src/uu/ls/src/colors.rs @@ -786,7 +786,7 @@ fn parse_indicator_codes() -> (FxHashMap, bool) { } fn canonicalize_indicator_value(value: &str) -> Cow<'_, str> { - if value.len() == 1 && value.chars().all(|c| c.is_ascii_digit()) { + if value.len() == 1 && value.as_bytes()[0].is_ascii_digit() { let mut canonical = String::with_capacity(2); canonical.push('0'); canonical.push_str(value); From f56ffd534453c15323b51793b42a1ef68060f9c6 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 22 Mar 2026 22:19:18 +0100 Subject: [PATCH 050/264] ls: simplify dired helpers --- src/uu/ls/src/dired.rs | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/src/uu/ls/src/dired.rs b/src/uu/ls/src/dired.rs index 4d96c4d9c27..e3cda96b54f 100644 --- a/src/uu/ls/src/dired.rs +++ b/src/uu/ls/src/dired.rs @@ -108,7 +108,7 @@ pub fn print_dired_output( } /// Helper function to print positions with a given prefix. -fn print_positions(prefix: &str, positions: &Vec) { +fn print_positions(prefix: &str, positions: &[BytePosition]) { print!("{prefix}"); for c in positions { print!(" {c}"); @@ -117,16 +117,7 @@ fn print_positions(prefix: &str, positions: &Vec) { } pub fn add_total(dired: &mut DiredOutput, total_len: usize) { - if dired.padding == 0 { - // when dealing with " total: xx", it isn't part of the //DIRED// - // so, we just keep the size line to add it to the position of the next file - dired.padding = total_len + DIRED_TRAILING_OFFSET; - } else { - // += because if we are in -R, we have " dir:\n total X". So, we need to take the - // previous padding too. - // and we already have the previous position in mind - dired.padding += total_len + DIRED_TRAILING_OFFSET; - } + dired.padding += total_len + DIRED_TRAILING_OFFSET; } // when using -R, we have the dirname. we need to add it to the padding @@ -156,7 +147,7 @@ pub fn update_positions(dired: &mut DiredOutput, start: usize, end: usize, line_ start: start + padding, end: end + padding, }); - dired.line_offset = dired.line_offset + padding + line_len; + dired.line_offset += padding + line_len; // Remove the previous padding dired.padding = 0; } From bfdeaccd655cf0d092aa5a64433b57a8c1cea626 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 22 Mar 2026 22:19:19 +0100 Subject: [PATCH 051/264] ls: inline calculate_line_len into update_dired_for_item --- src/uu/ls/src/display.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/uu/ls/src/display.rs b/src/uu/ls/src/display.rs index 1695eefc566..929ce7e4610 100644 --- a/src/uu/ls/src/display.rs +++ b/src/uu/ls/src/display.rs @@ -557,10 +557,6 @@ fn display_additional_leading_info( Ok(()) } -fn calculate_line_len(output_len: usize, item_len: usize) -> usize { - output_len + item_len + 1 // line ending -} - // Currently getpwuid is `linux` target only. If it's broken state.out into // a posix-compliant attribute this can be updated... #[cfg(unix)] @@ -1210,7 +1206,7 @@ fn update_dired_for_item( displayed_len: usize, dired_name_len: usize, ) { - let line_len = calculate_line_len(output_display_len, displayed_len); + let line_len = output_display_len + displayed_len + 1; // +1 for line ending dired::calculate_and_update_positions(dired, output_display_len, dired_name_len, line_len); } From 58b9e182a1bd26b92f0ee94047c28593f5cac046 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Mon, 23 Mar 2026 16:43:21 +0900 Subject: [PATCH 052/264] uucore: drop an unused function --- src/uucore/src/lib/features/pipes.rs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/uucore/src/lib/features/pipes.rs b/src/uucore/src/lib/features/pipes.rs index 5ac590b7b4a..069736c5215 100644 --- a/src/uucore/src/lib/features/pipes.rs +++ b/src/uucore/src/lib/features/pipes.rs @@ -7,8 +7,6 @@ use std::fs::File; #[cfg(any(target_os = "linux", target_os = "android"))] -use std::io::IoSlice; -#[cfg(any(target_os = "linux", target_os = "android"))] use std::os::fd::AsFd; #[cfg(any(target_os = "linux", target_os = "android"))] @@ -54,11 +52,3 @@ pub fn splice_exact(source: &impl AsFd, target: &impl AsFd, len: usize) -> Resul } Ok(()) } - -/// Copy data from `bytes` into `target`, which must be a pipe. -/// -/// Returns the number of successfully copied bytes. -#[cfg(any(target_os = "linux", target_os = "android"))] -pub fn vmsplice(target: &impl AsFd, bytes: &[u8]) -> Result { - nix::fcntl::vmsplice(target, &[IoSlice::new(bytes)], SpliceFFlags::empty()) -} From c58937767b307f44b962a47abd4ec506f78ac828 Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Mon, 23 Mar 2026 11:26:09 +0100 Subject: [PATCH 053/264] cut: use "?" instead of "match" --- src/uu/cut/src/matcher.rs | 40 +++++++++++++++----------------------- src/uu/cut/src/searcher.rs | 13 +++++-------- 2 files changed, 21 insertions(+), 32 deletions(-) diff --git a/src/uu/cut/src/matcher.rs b/src/uu/cut/src/matcher.rs index b4294129442..c1be9fb5ee7 100644 --- a/src/uu/cut/src/matcher.rs +++ b/src/uu/cut/src/matcher.rs @@ -27,20 +27,14 @@ impl Matcher for ExactMatcher<'_> { fn next_match(&self, haystack: &[u8]) -> Option<(usize, usize)> { let mut pos = 0usize; loop { - match memchr(self.needle[0], &haystack[pos..]) { - Some(match_idx) => { - let match_idx = match_idx + pos; // account for starting from pos - if self.needle.len() == 1 - || haystack[match_idx + 1..].starts_with(&self.needle[1..]) - { - return Some((match_idx, match_idx + self.needle.len())); - } - pos = match_idx + 1; - } - None => { - return None; - } + let match_idx = memchr(self.needle[0], &haystack[pos..])?; + let match_idx = match_idx + pos; // account for starting from pos + + if self.needle.len() == 1 || haystack[match_idx + 1..].starts_with(&self.needle[1..]) { + return Some((match_idx, match_idx + self.needle.len())); } + + pos = match_idx + 1; } } } @@ -50,19 +44,17 @@ pub struct WhitespaceMatcher {} impl Matcher for WhitespaceMatcher { fn next_match(&self, haystack: &[u8]) -> Option<(usize, usize)> { - match memchr2(b' ', b'\t', haystack) { - Some(match_idx) => { - let mut skip = match_idx + 1; - while skip < haystack.len() { - match haystack[skip] { - b' ' | b'\t' => skip += 1, - _ => break, - } - } - Some((match_idx, skip)) + let match_idx = memchr2(b' ', b'\t', haystack)?; + let mut skip = match_idx + 1; + + while skip < haystack.len() { + match haystack[skip] { + b' ' | b'\t' => skip += 1, + _ => break, } - None => None, } + + Some((match_idx, skip)) } } diff --git a/src/uu/cut/src/searcher.rs b/src/uu/cut/src/searcher.rs index dc252d804f7..a25fa7909a1 100644 --- a/src/uu/cut/src/searcher.rs +++ b/src/uu/cut/src/searcher.rs @@ -31,14 +31,11 @@ impl Iterator for Searcher<'_, '_, M> { type Item = (usize, usize); fn next(&mut self) -> Option { - match self.matcher.next_match(&self.haystack[self.position..]) { - Some((first, last)) => { - let result = (first + self.position, last + self.position); - self.position += last; - Some(result) - } - None => None, - } + let (first, last) = self.matcher.next_match(&self.haystack[self.position..])?; + let result = (first + self.position, last + self.position); + self.position += last; + + Some(result) } } From d07c42f67b4958da09c8be44ffd8de484eef9455 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 12:05:40 +0000 Subject: [PATCH 054/264] chore(deps): update rust crate zip to v8.4.0 --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1a6a237c6e2..bb5ff0610b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5181,9 +5181,9 @@ dependencies = [ [[package]] name = "zip" -version = "8.3.1" +version = "8.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c546feb4481b0fbafb4ef0d79b6204fc41c6f9884b1b73b1d73f82442fc0845" +checksum = "7756d0206d058333667493c4014f545f4b9603c4330ccd6d9b3f86dcab59f7d9" dependencies = [ "crc32fast", "flate2", From bfc3c4e89ac9e8f34d6f82ecf5d96d14dcc66ad0 Mon Sep 17 00:00:00 2001 From: xtqqczze <45661989+xtqqczze@users.noreply.github.com> Date: Mon, 23 Mar 2026 13:06:31 +0000 Subject: [PATCH 055/264] refactor(base32): use filter_map --- src/uu/base32/src/base_common.rs | 50 +++++++++++++++----------------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/src/uu/base32/src/base_common.rs b/src/uu/base32/src/base_common.rs index 0c7e13c23c0..4e82986737f 100644 --- a/src/uu/base32/src/base_common.rs +++ b/src/uu/base32/src/base_common.rs @@ -455,39 +455,37 @@ pub mod fast_encode { .iter() .enumerate() .step_by(encode_in_chunks_of_size) - .map(|(idx, _)| { + .filter_map(|(idx, _)| { // The part of `input_buffer` that was actually filled by the call // to `read` - &input[idx..min(input_size, idx + encode_in_chunks_of_size)] - }) - .map(|buffer| { + let buffer = &input[idx..min(input_size, idx + encode_in_chunks_of_size)]; + if buffer.len() < encode_in_chunks_of_size { leftover_buffer.extend(buffer); assert!(leftover_buffer.len() < encode_in_chunks_of_size); - return None; + None + } else { + Some(buffer) } - Some(buffer) }) - .for_each(|buffer| { - if let Some(read_buffer) = buffer { - // Encode data in chunks, then place it in `encoded_buffer` - assert_eq!(read_buffer.len(), encode_in_chunks_of_size); - encode_in_chunks_to_buffer( - supports_fast_decode_and_encode, - read_buffer, - &mut encoded_buffer, - ) - .unwrap(); - // Write all data in `encoded_buffer` to `output` - write_to_output( - &mut line_wrapping, - &mut encoded_buffer, - output, - false, - wrap == Some(0), - ) - .unwrap(); - } + .for_each(|read_buffer| { + // Encode data in chunks, then place it in `encoded_buffer` + assert_eq!(read_buffer.len(), encode_in_chunks_of_size); + encode_in_chunks_to_buffer( + supports_fast_decode_and_encode, + read_buffer, + &mut encoded_buffer, + ) + .unwrap(); + // Write all data in `encoded_buffer` to `output` + write_to_output( + &mut line_wrapping, + &mut encoded_buffer, + output, + false, + wrap == Some(0), + ) + .unwrap(); }); // Cleanup From a3487081b5e9011baeae9d1188207992ad24c8b6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 18:40:34 +0000 Subject: [PATCH 056/264] chore(deps): update rust crate terminal_size to v0.4.4 --- Cargo.lock | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bb5ff0610b2..de54b50704f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -992,7 +992,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -1709,7 +1709,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde_core", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -2012,7 +2012,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -2605,7 +2605,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -2905,17 +2905,17 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] name = "terminal_size" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" +checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -4693,7 +4693,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] From 13e26b9f218edc9655b619444ae6d92841531926 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Tue, 24 Mar 2026 13:07:46 +0900 Subject: [PATCH 057/264] tee: reduce if block --- src/uu/tee/src/tee.rs | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/src/uu/tee/src/tee.rs b/src/uu/tee/src/tee.rs index b9912c42ebf..344286aa662 100644 --- a/src/uu/tee/src/tee.rs +++ b/src/uu/tee/src/tee.rs @@ -231,22 +231,18 @@ fn copy(mut input: impl Read, mut output: impl Write) -> Result { let mut len = 0; loop { - let received = match input.read(&mut buffer) { - Ok(bytes_count) => bytes_count, - Err(e) if e.kind() == ErrorKind::Interrupted => continue, + match input.read(&mut buffer) { + Ok(0) => return Ok(len), // end of file + Ok(received) => { + output.write_all(&buffer[..received])?; + // flush the buffer to comply with POSIX requirement that + // `tee` does not buffer the input. + output.flush()?; + len += received; + } + Err(e) if e.kind() == ErrorKind::Interrupted => {} Err(e) => return Err(e), - }; - - if received == 0 { - return Ok(len); } - - output.write_all(&buffer[0..received])?; - - // We need to flush the buffer here to comply with POSIX requirement that - // `tee` does not buffer the input. - output.flush()?; - len += received; } } From 9abe08e6fd36d3c207b8304a39ecade009f31fb2 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Wed, 25 Mar 2026 16:24:32 +0900 Subject: [PATCH 058/264] yes, coreutils: backport(enable) 2 GnuTests (#11480) --- .github/workflows/GnuTests.yml | 2 +- .vscode/cspell.dictionaries/workspace.wordlist.txt | 2 ++ src/uu/yes/src/yes.rs | 4 ++-- util/build-gnu.sh | 2 +- util/fetch-gnu.sh | 13 +++++++++---- 5 files changed, 15 insertions(+), 8 deletions(-) diff --git a/.github/workflows/GnuTests.yml b/.github/workflows/GnuTests.yml index 2ad489c8f18..e350c31bcf3 100644 --- a/.github/workflows/GnuTests.yml +++ b/.github/workflows/GnuTests.yml @@ -2,7 +2,7 @@ name: GnuTests # spell-checker:ignore (abbrev/names) CodeCov gnulib GnuTests Swatinem # spell-checker:ignore (jargon) submodules devel -# spell-checker:ignore (libs/utils) chksum dpkg getenforce getlimits gperf lcov libexpect limactl pyinotify setenforce shopt valgrind libattr libcap taiki-e zstd cpio +# spell-checker:ignore (libs/utils) chksum dpkg getenforce gperf lcov libexpect limactl pyinotify setenforce shopt valgrind libattr libcap taiki-e zstd cpio # spell-checker:ignore (options) Ccodegen Coverflow Cpanic Zpanic # spell-checker:ignore (people) Dawid Dziurla * dawidd dtolnay # spell-checker:ignore (vars) FILESET SUBDIRS XPASS diff --git a/.vscode/cspell.dictionaries/workspace.wordlist.txt b/.vscode/cspell.dictionaries/workspace.wordlist.txt index 1d3929832a2..a5d245016c7 100644 --- a/.vscode/cspell.dictionaries/workspace.wordlist.txt +++ b/.vscode/cspell.dictionaries/workspace.wordlist.txt @@ -138,6 +138,7 @@ EEXIST EINVAL ENODATA ENOENT +ENOSPC ENOSYS ENOTEMPTY EOPNOTSUPP @@ -363,6 +364,7 @@ uutils getcwd # * other +getlimits weblate algs wasm diff --git a/src/uu/yes/src/yes.rs b/src/uu/yes/src/yes.rs index 88d2c7ed113..732a1bf5877 100644 --- a/src/uu/yes/src/yes.rs +++ b/src/uu/yes/src/yes.rs @@ -9,7 +9,7 @@ use clap::{Arg, ArgAction, Command, builder::ValueParser}; use std::error::Error; use std::ffi::OsString; use std::io::{self, Write}; -use uucore::error::{UResult, USimpleError}; +use uucore::error::{UResult, USimpleError, strip_errno}; use uucore::format_usage; use uucore::translate; @@ -33,7 +33,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { Err(err) if err.kind() == io::ErrorKind::BrokenPipe => Ok(()), Err(err) => Err(USimpleError::new( 1, - translate!("yes-error-standard-output", "error" => err), + translate!("yes-error-standard-output", "error" => strip_errno(&err)), )), } } diff --git a/util/build-gnu.sh b/util/build-gnu.sh index 4a731f8aa29..3f6ec60d817 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -2,7 +2,7 @@ # `build-gnu.bash` ~ builds GNU coreutils (from supplied sources) # -# spell-checker:ignore (paths) abmon deref discrim eacces getlimits getopt ginstall inacc infloop inotify reflink ; (misc) INT_OFLOW OFLOW +# spell-checker:ignore (paths) abmon deref discrim eacces getopt ginstall inacc infloop inotify reflink ; (misc) INT_OFLOW OFLOW # spell-checker:ignore baddecode submodules xstrtol distros ; (vars/env) SRCDIR vdir rcexp xpart dired OSTYPE ; (utils) greadlink gsed multihardlink texinfo CARGOFLAGS # spell-checker:ignore openat TOCTOU CFLAGS tmpfs gnproc diff --git a/util/fetch-gnu.sh b/util/fetch-gnu.sh index afd9ebfa88a..ae6de16e7fc 100755 --- a/util/fetch-gnu.sh +++ b/util/fetch-gnu.sh @@ -4,7 +4,12 @@ repo=https://github.com/coreutils/coreutils curl -L "${repo}/releases/download/v${ver}/coreutils-${ver}.tar.xz" | tar --strip-components=1 -xJf - # TODO stop backporting tests from master at GNU coreutils > $ver -# backport = () -# for f in ${backport[@]} -# do curl -L ${repo}/raw/refs/heads/master/tests/$f > tests/$f -# done + backport=( + misc/coreutils.sh # enable test + misc/yes.sh # zero-copy +) + for f in "${backport[@]}" + do curl -L ${repo}/raw/refs/heads/master/tests/$f > tests/$f + done +# adjust for getlimits > $ver +sed -i.b "s/\$ENOSPC/No space left on device/" tests/misc/yes.sh From 4eb967b90f614edb02a54805e820e3d15806dea5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dorian=20P=C3=A9ron?= Date: Fri, 20 Mar 2026 19:20:22 +0100 Subject: [PATCH 059/264] cksum: Refactor Blake length validation --- src/uu/b2sum/src/b2sum.rs | 5 +- src/uu/checksum_common/src/lib.rs | 5 +- src/uu/cksum/src/cksum.rs | 4 +- src/uucore/src/lib/features/checksum/mod.rs | 122 +++++++++--------- .../src/lib/features/checksum/validate.rs | 16 ++- src/uucore/src/lib/features/sum.rs | 6 + 6 files changed, 87 insertions(+), 71 deletions(-) diff --git a/src/uu/b2sum/src/b2sum.rs b/src/uu/b2sum/src/b2sum.rs index ddd5fe3e9e6..66227664192 100644 --- a/src/uu/b2sum/src/b2sum.rs +++ b/src/uu/b2sum/src/b2sum.rs @@ -9,13 +9,14 @@ use clap::Command; use uu_checksum_common::{standalone_checksum_app_with_length, standalone_with_length_main}; -use uucore::checksum::{AlgoKind, calculate_blake_length_str}; +use uucore::checksum::{AlgoKind, BlakeLength, parse_blake_length}; use uucore::error::UResult; use uucore::translate; #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let calculate_blake2b_length = |s: &str| calculate_blake_length_str(AlgoKind::Blake2b, s); + let calculate_blake2b_length = + |s: &str| parse_blake_length(AlgoKind::Blake2b, BlakeLength::String(s)); standalone_with_length_main(AlgoKind::Blake2b, uu_app(), args, calculate_blake2b_length) } diff --git a/src/uu/checksum_common/src/lib.rs b/src/uu/checksum_common/src/lib.rs index f0d307870e7..56ded13f19e 100644 --- a/src/uu/checksum_common/src/lib.rs +++ b/src/uu/checksum_common/src/lib.rs @@ -63,7 +63,7 @@ pub fn standalone_with_length_main( algo: AlgoKind, cmd: Command, args: impl uucore::Args, - validate_len: fn(&str) -> UResult>, + validate_len: fn(&str) -> UResult, ) -> UResult<()> { let matches = uucore::clap_localization::handle_clap_result(cmd, args)?; let algo = Some(algo); @@ -72,8 +72,7 @@ pub fn standalone_with_length_main( .get_one::(options::LENGTH) .map(String::as_str) .map(validate_len) - .transpose()? - .flatten(); + .transpose()?; //todo: deduplicate matches.get_flag let text = !matches.get_flag(options::BINARY); diff --git a/src/uu/cksum/src/cksum.rs b/src/uu/cksum/src/cksum.rs index 19898def4f9..df3f46a14a0 100644 --- a/src/uu/cksum/src/cksum.rs +++ b/src/uu/cksum/src/cksum.rs @@ -12,7 +12,7 @@ use uu_checksum_common::{ChecksumCommand, checksum_main, default_checksum_app, o use uucore::checksum::compute::OutputFormat; use uucore::checksum::{ - AlgoKind, ChecksumError, calculate_blake_length_str, sanitize_sha2_sha3_length_str, + AlgoKind, BlakeLength, ChecksumError, parse_blake_length, sanitize_sha2_sha3_length_str, }; use uucore::error::UResult; use uucore::hardware::{HasHardwareFeatures as _, SimdPolicy}; @@ -69,7 +69,7 @@ fn maybe_sanitize_length( // For BLAKE, if a length is provided, validate it. (Some(algo @ (AlgoKind::Blake2b | AlgoKind::Blake3)), Some(len)) => { - calculate_blake_length_str(algo, len) + parse_blake_length(algo, BlakeLength::String(len)).map(Some) } // For any other provided algorithm, check if length is 0. diff --git a/src/uucore/src/lib/features/checksum/mod.rs b/src/uucore/src/lib/features/checksum/mod.rs index 9433bbf3682..e6145d3748e 100644 --- a/src/uucore/src/lib/features/checksum/mod.rs +++ b/src/uucore/src/lib/features/checksum/mod.rs @@ -250,8 +250,8 @@ pub enum SizedAlgoKind { Sha2(ShaLength), Sha3(ShaLength), // Note: we store Blake*'s length as BYTES. - Blake2b(Option), - Blake3(Option), + Blake2b(usize), + Blake3(usize), // Shake* length are stored in bits. Shake128(Option), Shake256(Option), @@ -284,7 +284,8 @@ impl SizedAlgoKind { (ak::Sm3, _) => Ok(Self::Sm3), (ak::Sha1, _) => Ok(Self::Sha1), - (ak::Blake3, l) => Ok(Self::Blake3(l)), + (ak::Blake2b, l) => Ok(Self::Blake2b(l.unwrap_or(Blake2b::DEFAULT_BYTE_SIZE))), + (ak::Blake3, l) => Ok(Self::Blake3(l.unwrap_or(Blake3::DEFAULT_BYTE_SIZE))), (ak::Shake128, l) => Ok(Self::Shake128(l)), (ak::Shake256, l) => Ok(Self::Shake256(l)), (ak::Sha2, Some(l)) => Ok(Self::Sha2(ShaLength::try_from(l)?)), @@ -292,14 +293,6 @@ impl SizedAlgoKind { (algo @ (ak::Sha2 | ak::Sha3), None) => { Err(ChecksumError::LengthRequiredForSha(algo.to_lowercase().into()).into()) } - // [`calculate_blake2b_length`] expects a length in bits but we - // have a length in bytes. - (algo @ ak::Blake2b, Some(l)) => Ok(Self::Blake2b(calculate_blake_length_str( - algo, - &(8 * l).to_string(), - )?)), - (ak::Blake2b, None) => Ok(Self::Blake2b(None)), - (ak::Sha224, None) => Ok(Self::Sha2(ShaLength::Len224)), (ak::Sha256, None) => Ok(Self::Sha2(ShaLength::Len256)), (ak::Sha384, None) => Ok(Self::Sha2(ShaLength::Len384)), @@ -314,14 +307,9 @@ impl SizedAlgoKind { Self::Sha1 => "SHA1".into(), Self::Sha2(len) => format!("SHA{}", len.as_usize()), Self::Sha3(len) => format!("SHA3-{}", len.as_usize()), - Self::Blake2b(Some(byte_len)) => format!("BLAKE2b-{}", byte_len * 8), - Self::Blake2b(None) => "BLAKE2b".into(), - Self::Blake3(byte_len) => { - format!( - "BLAKE3-{}", - byte_len.unwrap_or(Blake3::DEFAULT_BYTE_SIZE) * 8 - ) - } + Self::Blake2b(Blake2b::DEFAULT_BYTE_SIZE) => "BLAKE2b".into(), + Self::Blake2b(byte_len) => format!("BLAKE2b-{}", byte_len * 8), + Self::Blake3(byte_len) => format!("BLAKE3-{}", byte_len * 8), Self::Shake128(opt_bit_len) => format!( "SHAKE128-{}", opt_bit_len.unwrap_or(Shake128::DEFAULT_BIT_SIZE) @@ -354,12 +342,8 @@ impl SizedAlgoKind { Self::Sha3(Len256) => Box::new(Sha3_256::default()), Self::Sha3(Len384) => Box::new(Sha3_384::default()), Self::Sha3(Len512) => Box::new(Sha3_512::default()), - Self::Blake2b(len_opt) => { - Box::new(len_opt.map(Blake2b::with_output_bytes).unwrap_or_default()) - } - Self::Blake3(len_opt) => { - Box::new(len_opt.map(Blake3::with_output_bytes).unwrap_or_default()) - } + Self::Blake2b(len) => Box::new(Blake2b::with_output_bytes(*len)), + Self::Blake3(len) => Box::new(Blake3::with_output_bytes(*len)), Self::Shake128(len_opt) => { Box::new(len_opt.map(Shake128::with_output_bits).unwrap_or_default()) } @@ -378,10 +362,10 @@ impl SizedAlgoKind { Self::Md5 => 128, Self::Sm3 => 512, Self::Sha1 => 160, - Self::Blake3(len) => len.unwrap_or(Blake3::DEFAULT_BYTE_SIZE) * 8, Self::Sha2(len) => len.as_usize(), Self::Sha3(len) => len.as_usize(), - Self::Blake2b(len) => len.unwrap_or(Blake2b::DEFAULT_BYTE_SIZE * 8), + Self::Blake2b(len) => len * 8, + Self::Blake3(len) => len * 8, Self::Shake128(len) => len.unwrap_or(Shake128::DEFAULT_BIT_SIZE), Self::Shake256(len) => len.unwrap_or(Shake256::DEFAULT_BIT_SIZE), } @@ -495,37 +479,57 @@ pub fn digest_reader( Ok((digest.result(), output_size)) } -/// Calculates the BYTE length of the digest. -pub fn calculate_blake_length_str(algo: AlgoKind, bit_length: &str) -> UResult> { - debug_assert!(matches!(algo, AlgoKind::Blake2b | AlgoKind::Blake3)); +pub enum BlakeLength<'s> { + Int(usize), + String(&'s str), +} - // Blake2b's length is parsed in an u64. - match bit_length.parse::() { - Ok(0) => Ok(None), +/// Expects a size in BITS, either as a string or int, and returns it as a BYTE +/// length. +/// +/// Note: when the input is a string, validation may print error messages. +/// Note: when the algo is Blake2b, values that are above 512 +/// (Blake2b::DEFAULT_BIT_SIZE) are errors. +pub fn parse_blake_length(algo: AlgoKind, bit_length: BlakeLength<'_>) -> UResult { + debug_assert!(matches!(algo, AlgoKind::Blake2b | AlgoKind::Blake3)); - // Error cases - Ok(n) if n > 512 && algo == AlgoKind::Blake2b => { - show_error!("{}", ChecksumError::InvalidLength(bit_length.into())); - Err(ChecksumError::LengthTooBigForBlake(algo.to_uppercase().into()).into()) - } - Err(e) if *e.kind() == IntErrorKind::PosOverflow => { - show_error!("{}", ChecksumError::InvalidLength(bit_length.into())); - Err(ChecksumError::LengthTooBigForBlake(algo.to_uppercase().into()).into()) + let print_error = || { + if let BlakeLength::String(s) = bit_length { + show_error!("{}", ChecksumError::InvalidLength(s.to_string())); } - Err(_) => Err(ChecksumError::InvalidLength(bit_length.into()).into()), + }; - Ok(n) if n % 8 != 0 => { - show_error!("{}", ChecksumError::InvalidLength(bit_length.into())); - Err(ChecksumError::LengthNotMultipleOf8.into()) - } + let n = match bit_length { + BlakeLength::Int(i) => i, + BlakeLength::String(s) => s.parse::().map_err(|e| { + if *e.kind() == IntErrorKind::PosOverflow { + print_error(); + ChecksumError::LengthTooBigForBlake(algo.to_uppercase().into()) + } else { + ChecksumError::InvalidLength(s.to_string()) + } + })?, + }; - // Valid cases + if n == 0 { + return Ok(match algo { + AlgoKind::Blake2b => Blake2b::DEFAULT_BYTE_SIZE, + AlgoKind::Blake3 => Blake3::DEFAULT_BYTE_SIZE, + _ => unreachable!(), + }); + } + + if algo == AlgoKind::Blake2b && n > Blake2b::DEFAULT_BIT_SIZE { + print_error(); + return Err(ChecksumError::LengthTooBigForBlake(algo.to_uppercase().into()).into()); + } - // When length is 512, it is blake2b's default. So, don't show it - Ok(512) => Ok(None), - // Divide by 8, as our blake2b implementation expects bytes instead of bits. - Ok(n) => Ok(Some(n / 8)), + if n % 8 != 0 { + print_error(); + return Err(ChecksumError::LengthNotMultipleOf8.into()); } + + Ok(n / 8) } pub fn validate_sha2_sha3_length(algo_name: AlgoKind, length: Option) -> UResult { @@ -644,18 +648,18 @@ mod tests { #[test] fn test_calculate_blake2b_length() { assert_eq!( - calculate_blake_length_str(AlgoKind::Blake2b, "0").unwrap(), - None + parse_blake_length(AlgoKind::Blake2b, BlakeLength::String("0")).unwrap(), + Blake2b::DEFAULT_BYTE_SIZE ); - assert!(calculate_blake_length_str(AlgoKind::Blake2b, "10").is_err()); - assert!(calculate_blake_length_str(AlgoKind::Blake2b, "520").is_err()); + assert!(parse_blake_length(AlgoKind::Blake2b, BlakeLength::String("10")).is_err()); + assert!(parse_blake_length(AlgoKind::Blake2b, BlakeLength::String("520")).is_err()); assert_eq!( - calculate_blake_length_str(AlgoKind::Blake2b, "512").unwrap(), - None + parse_blake_length(AlgoKind::Blake2b, BlakeLength::String("512")).unwrap(), + Blake2b::DEFAULT_BYTE_SIZE ); assert_eq!( - calculate_blake_length_str(AlgoKind::Blake2b, "256").unwrap(), - Some(32) + parse_blake_length(AlgoKind::Blake2b, BlakeLength::String("256")).unwrap(), + 32 ); } } diff --git a/src/uucore/src/lib/features/checksum/validate.rs b/src/uucore/src/lib/features/checksum/validate.rs index fcfba47e8c0..fa51539fe1e 100644 --- a/src/uucore/src/lib/features/checksum/validate.rs +++ b/src/uucore/src/lib/features/checksum/validate.rs @@ -15,11 +15,12 @@ use std::io::{self, BufReader, Read, Write, stderr, stdin}; use os_display::Quotable; use crate::checksum::{ - AlgoKind, ChecksumError, ReadingMode, SizedAlgoKind, digest_reader, unescape_filename, + AlgoKind, BlakeLength, ChecksumError, ReadingMode, SizedAlgoKind, digest_reader, + parse_blake_length, unescape_filename, }; use crate::error::{FromIo, UError, UIoError, UResult, USimpleError}; use crate::quoting_style::{QuotingStyle, locale_aware_escape_name}; -use crate::sum::{self, DigestOutput}; +use crate::sum::{self, Blake2b, Blake3, DigestOutput}; use crate::{ os_str_as_bytes, os_str_from_bytes, read_os_string_lines, show, show_warning_caps, translate, }; @@ -637,7 +638,12 @@ fn identify_algo_name_and_length( let bytes = if let Some(bitlen) = line_info.algo_bit_len { match line_algo { - ak::Blake2b | ak::Blake3 if bitlen % 8 == 0 => Some(bitlen / 8), + algo @ (ak::Blake2b | ak::Blake3) => { + match parse_blake_length(algo, BlakeLength::Int(bitlen)) { + Ok(len) => Some(len), + Err(_) => return Err(LineCheckError::ImproperlyFormatted), + } + } ak::Sha2 | ak::Sha3 if [224, 256, 384, 512].contains(&bitlen) => Some(bitlen), ak::Shake128 | ak::Shake256 => Some(bitlen), // Either @@ -653,10 +659,10 @@ fn identify_algo_name_and_length( } } else if line_algo == ak::Blake2b { // Default length with BLAKE2b, - Some(64) + Some(Blake2b::DEFAULT_BYTE_SIZE) } else if line_algo == ak::Blake3 { // Default length with BLAKE3, - Some(32) + Some(Blake3::DEFAULT_BYTE_SIZE) } else { None }; diff --git a/src/uucore/src/lib/features/sum.rs b/src/uucore/src/lib/features/sum.rs index af122ced1a3..5c998cd0764 100644 --- a/src/uucore/src/lib/features/sum.rs +++ b/src/uucore/src/lib/features/sum.rs @@ -83,9 +83,15 @@ pub struct Blake2b { impl Blake2b { pub const DEFAULT_BYTE_SIZE: usize = 64; + pub const DEFAULT_BIT_SIZE: usize = Self::DEFAULT_BYTE_SIZE * 8; /// Return a new Blake2b instance with a custom output bytes length pub fn with_output_bytes(output_bytes: usize) -> Self { + debug_assert!( + output_bytes <= Self::DEFAULT_BYTE_SIZE, + "GNU doesn't accept BLAKE2b bigger than 64 bytes long" + ); + let mut params = blake2b_simd::Params::new(); params.hash_length(output_bytes); From cdf04d3ab41651e69812c26710b1c8a3d04f5dd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dorian=20P=C3=A9ron?= Date: Sat, 21 Mar 2026 03:20:19 +0100 Subject: [PATCH 060/264] test(cksum): use rstest template to generate isolated tests for each algo... ...on applicable tests --- tests/by-util/test_cksum.rs | 192 ++++++++++++++++++------------------ 1 file changed, 94 insertions(+), 98 deletions(-) diff --git a/tests/by-util/test_cksum.rs b/tests/by-util/test_cksum.rs index 1ab5bb45285..e1ffbb68fed 100644 --- a/tests/by-util/test_cksum.rs +++ b/tests/by-util/test_cksum.rs @@ -13,11 +13,23 @@ use uutests::util::TestScenario; use uutests::util::log_info; use uutests::util_name; -const ALGOS: [&str; 11] = [ - "sysv", "bsd", "crc", "md5", "sha1", "sha224", "sha256", "sha384", "sha512", "blake2b", "sm3", -]; const SHA_LENGTHS: [u32; 4] = [224, 256, 384, 512]; +#[template] +#[rstest] +#[case::sysv("sysv")] +#[case::bsd("bsd")] +#[case::crc("crc")] +#[case::md5("md5")] +#[case::sha1("sha1")] +#[case::sha224("sha224")] +#[case::sha256("sha256")] +#[case::sha384("sha384")] +#[case::sha512("sha512")] +#[case::blake2b("blake2b")] +#[case::sm3("sm3")] +fn test_all_algos(#[case] algo: &str) {} + #[test] fn test_invalid_arg() { new_ucmd!().arg("--definitely-invalid").fails_with_code(1); @@ -170,43 +182,37 @@ fn test_tag_after_untagged() { .stdout_is_fixture("md5_single_file.expected"); } -#[test] -fn test_algorithm_single_file() { - for algo in ALGOS { - for option in ["-a", "--algorithm"] { - new_ucmd!() - .arg(format!("{option}={algo}")) - .arg("lorem_ipsum.txt") - .succeeds() - .stdout_is_fixture(format!("{algo}_single_file.expected")); - } +#[apply(test_all_algos)] +fn test_algorithm_single_file(#[case] algo: &str) { + for option in ["-a", "--algorithm"] { + new_ucmd!() + .arg(format!("{option}={algo}")) + .arg("lorem_ipsum.txt") + .succeeds() + .stdout_is_fixture(format!("{algo}_single_file.expected")); } } -#[test] -fn test_algorithm_multiple_files() { - for algo in ALGOS { - for option in ["-a", "--algorithm"] { - new_ucmd!() - .arg(format!("{option}={algo}")) - .arg("lorem_ipsum.txt") - .arg("alice_in_wonderland.txt") - .succeeds() - .stdout_is_fixture(format!("{algo}_multiple_files.expected")); - } +#[apply(test_all_algos)] +fn test_algorithm_multiple_files(#[case] algo: &str) { + for option in ["-a", "--algorithm"] { + new_ucmd!() + .arg(format!("{option}={algo}")) + .arg("lorem_ipsum.txt") + .arg("alice_in_wonderland.txt") + .succeeds() + .stdout_is_fixture(format!("{algo}_multiple_files.expected")); } } -#[test] -fn test_algorithm_stdin() { - for algo in ALGOS { - for option in ["-a", "--algorithm"] { - new_ucmd!() - .arg(format!("{option}={algo}")) - .pipe_in_fixture("lorem_ipsum.txt") - .succeeds() - .stdout_is_fixture(format!("{algo}_stdin.expected")); - } +#[apply(test_all_algos)] +fn test_algorithm_stdin(#[case] algo: &str) { + for option in ["-a", "--algorithm"] { + new_ucmd!() + .arg(format!("{option}={algo}")) + .pipe_in_fixture("lorem_ipsum.txt") + .succeeds() + .stdout_is_fixture(format!("{algo}_stdin.expected")); } } @@ -238,16 +244,14 @@ fn test_untagged_stdin() { .stdout_is_fixture("untagged/crc_stdin.expected"); } -#[test] -fn test_untagged_algorithm_single_file() { - for algo in ALGOS { - new_ucmd!() - .arg("--untagged") - .arg(format!("--algorithm={algo}")) - .arg("lorem_ipsum.txt") - .succeeds() - .stdout_is_fixture(format!("untagged/{algo}_single_file.expected")); - } +#[apply(test_all_algos)] +fn test_untagged_algorithm_single_file(#[case] algo: &str) { + new_ucmd!() + .arg("--untagged") + .arg(format!("--algorithm={algo}")) + .arg("lorem_ipsum.txt") + .succeeds() + .stdout_is_fixture(format!("untagged/{algo}_single_file.expected")); } #[test] @@ -272,29 +276,25 @@ fn test_untagged_algorithm_after_tag() { .stdout_is_fixture("untagged/md5_single_file.expected"); } -#[test] -fn test_untagged_algorithm_multiple_files() { - for algo in ALGOS { - new_ucmd!() - .arg("--untagged") - .arg(format!("--algorithm={algo}")) - .arg("lorem_ipsum.txt") - .arg("alice_in_wonderland.txt") - .succeeds() - .stdout_is_fixture(format!("untagged/{algo}_multiple_files.expected")); - } +#[apply(test_all_algos)] +fn test_untagged_algorithm_multiple_files(#[case] algo: &str) { + new_ucmd!() + .arg("--untagged") + .arg(format!("--algorithm={algo}")) + .arg("lorem_ipsum.txt") + .arg("alice_in_wonderland.txt") + .succeeds() + .stdout_is_fixture(format!("untagged/{algo}_multiple_files.expected")); } -#[test] -fn test_untagged_algorithm_stdin() { - for algo in ALGOS { - new_ucmd!() - .arg("--untagged") - .arg(format!("--algorithm={algo}")) - .pipe_in_fixture("lorem_ipsum.txt") - .succeeds() - .stdout_is_fixture(format!("untagged/{algo}_stdin.expected")); - } +#[apply(test_all_algos)] +fn test_untagged_algorithm_stdin(#[case] algo: &str) { + new_ucmd!() + .arg("--untagged") + .arg(format!("--algorithm={algo}")) + .pipe_in_fixture("lorem_ipsum.txt") + .succeeds() + .stdout_is_fixture(format!("untagged/{algo}_stdin.expected")); } #[test] @@ -846,18 +846,17 @@ fn test_blake2b_length_invalid() { } } -#[test] -fn test_raw_single_file() { - for algo in ALGOS { - new_ucmd!() - .arg("--raw") - .arg("lorem_ipsum.txt") - .arg(format!("--algorithm={algo}")) - .succeeds() - .no_stderr() - .stdout_is_fixture_bytes(format!("raw/{algo}_single_file.expected")); - } +#[apply(test_all_algos)] +fn test_raw_single_file(#[case] algo: &str) { + new_ucmd!() + .arg("--raw") + .arg("lorem_ipsum.txt") + .arg(format!("--algorithm={algo}")) + .succeeds() + .no_stderr() + .stdout_is_fixture_bytes(format!("raw/{algo}_single_file.expected")); } + #[test] fn test_raw_multiple_files() { new_ucmd!() @@ -882,18 +881,17 @@ fn test_base64_raw_conflicts() { .stderr_contains("--raw"); } -#[test] -fn test_base64_single_file() { - for algo in ALGOS { - new_ucmd!() - .arg("--base64") - .arg("lorem_ipsum.txt") - .arg(format!("--algorithm={algo}")) - .succeeds() - .no_stderr() - .stdout_is_fixture_bytes(format!("base64/{algo}_single_file.expected")); - } +#[apply(test_all_algos)] +fn test_base64_single_file(#[case] algo: &str) { + new_ucmd!() + .arg("--base64") + .arg("lorem_ipsum.txt") + .arg(format!("--algorithm={algo}")) + .succeeds() + .no_stderr() + .stdout_is_fixture_bytes(format!("base64/{algo}_single_file.expected")); } + #[test] fn test_base64_multiple_files() { new_ucmd!() @@ -919,8 +917,8 @@ fn test_fail_on_folder() { .stderr_contains(format!("cksum: {folder_name}: Is a directory")); } -#[test] -fn test_all_algorithms_fail_on_folder() { +#[apply(test_all_algos)] +fn test_all_algorithms_fail_on_folder(#[case] algo: &str) { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; @@ -928,15 +926,13 @@ fn test_all_algorithms_fail_on_folder() { let folder_name = "a_folder"; at.mkdir(folder_name); - for algo in ALGOS { - scene - .ucmd() - .arg(format!("--algorithm={algo}")) - .arg(folder_name) - .fails() - .no_stdout() - .stderr_contains(format!("cksum: {folder_name}: Is a directory")); - } + scene + .ucmd() + .arg(format!("--algorithm={algo}")) + .arg(folder_name) + .fails() + .no_stdout() + .stderr_contains(format!("cksum: {folder_name}: Is a directory")); } #[cfg(unix)] From 1393d9f3198a3cb82fe6f67ec6844c9a21d98c01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dorian=20P=C3=A9ron?= Date: Sat, 21 Mar 2026 03:34:24 +0100 Subject: [PATCH 061/264] test(cksum): add regular tests for blake3 --- tests/by-util/test_cksum.rs | 1 + tests/fixtures/cksum/base64/blake3_single_file.expected | 1 + tests/fixtures/cksum/blake3_multiple_files.expected | 2 ++ tests/fixtures/cksum/blake3_single_file.expected | 1 + tests/fixtures/cksum/blake3_stdin.expected | 1 + tests/fixtures/cksum/raw/blake3_single_file.expected | 1 + tests/fixtures/cksum/untagged/blake3_multiple_files.expected | 2 ++ tests/fixtures/cksum/untagged/blake3_single_file.expected | 1 + tests/fixtures/cksum/untagged/blake3_stdin.expected | 1 + 9 files changed, 11 insertions(+) create mode 100644 tests/fixtures/cksum/base64/blake3_single_file.expected create mode 100644 tests/fixtures/cksum/blake3_multiple_files.expected create mode 100644 tests/fixtures/cksum/blake3_single_file.expected create mode 100644 tests/fixtures/cksum/blake3_stdin.expected create mode 100644 tests/fixtures/cksum/raw/blake3_single_file.expected create mode 100644 tests/fixtures/cksum/untagged/blake3_multiple_files.expected create mode 100644 tests/fixtures/cksum/untagged/blake3_single_file.expected create mode 100644 tests/fixtures/cksum/untagged/blake3_stdin.expected diff --git a/tests/by-util/test_cksum.rs b/tests/by-util/test_cksum.rs index e1ffbb68fed..666ce4cc616 100644 --- a/tests/by-util/test_cksum.rs +++ b/tests/by-util/test_cksum.rs @@ -27,6 +27,7 @@ const SHA_LENGTHS: [u32; 4] = [224, 256, 384, 512]; #[case::sha384("sha384")] #[case::sha512("sha512")] #[case::blake2b("blake2b")] +#[case::blake3("blake3")] #[case::sm3("sm3")] fn test_all_algos(#[case] algo: &str) {} diff --git a/tests/fixtures/cksum/base64/blake3_single_file.expected b/tests/fixtures/cksum/base64/blake3_single_file.expected new file mode 100644 index 00000000000..cce3661ff3d --- /dev/null +++ b/tests/fixtures/cksum/base64/blake3_single_file.expected @@ -0,0 +1 @@ +BLAKE3-256 (lorem_ipsum.txt) = /c9QNtxcUOUQabtbQFNF7VC4iiTaZsb4shtGR3z2mdk= diff --git a/tests/fixtures/cksum/blake3_multiple_files.expected b/tests/fixtures/cksum/blake3_multiple_files.expected new file mode 100644 index 00000000000..f9541ed6b2c --- /dev/null +++ b/tests/fixtures/cksum/blake3_multiple_files.expected @@ -0,0 +1,2 @@ +BLAKE3-256 (lorem_ipsum.txt) = fdcf5036dc5c50e51069bb5b405345ed50b88a24da66c6f8b21b46477cf699d9 +BLAKE3-256 (alice_in_wonderland.txt) = 93c026644dc6b34a13a2b6705485d06f32d712c76ebe5353e935155edeeb49d5 diff --git a/tests/fixtures/cksum/blake3_single_file.expected b/tests/fixtures/cksum/blake3_single_file.expected new file mode 100644 index 00000000000..cfc017b947c --- /dev/null +++ b/tests/fixtures/cksum/blake3_single_file.expected @@ -0,0 +1 @@ +BLAKE3-256 (lorem_ipsum.txt) = fdcf5036dc5c50e51069bb5b405345ed50b88a24da66c6f8b21b46477cf699d9 diff --git a/tests/fixtures/cksum/blake3_stdin.expected b/tests/fixtures/cksum/blake3_stdin.expected new file mode 100644 index 00000000000..fbb69fbc1e9 --- /dev/null +++ b/tests/fixtures/cksum/blake3_stdin.expected @@ -0,0 +1 @@ +BLAKE3-256 (-) = fdcf5036dc5c50e51069bb5b405345ed50b88a24da66c6f8b21b46477cf699d9 diff --git a/tests/fixtures/cksum/raw/blake3_single_file.expected b/tests/fixtures/cksum/raw/blake3_single_file.expected new file mode 100644 index 00000000000..c9e56f00552 --- /dev/null +++ b/tests/fixtures/cksum/raw/blake3_single_file.expected @@ -0,0 +1 @@ +ýÏP6Ü\Påi»[@SEíP¸Š$ÚfÆø²FG|ö™Ù \ No newline at end of file diff --git a/tests/fixtures/cksum/untagged/blake3_multiple_files.expected b/tests/fixtures/cksum/untagged/blake3_multiple_files.expected new file mode 100644 index 00000000000..29379596a67 --- /dev/null +++ b/tests/fixtures/cksum/untagged/blake3_multiple_files.expected @@ -0,0 +1,2 @@ +fdcf5036dc5c50e51069bb5b405345ed50b88a24da66c6f8b21b46477cf699d9 lorem_ipsum.txt +93c026644dc6b34a13a2b6705485d06f32d712c76ebe5353e935155edeeb49d5 alice_in_wonderland.txt diff --git a/tests/fixtures/cksum/untagged/blake3_single_file.expected b/tests/fixtures/cksum/untagged/blake3_single_file.expected new file mode 100644 index 00000000000..1fafca356f4 --- /dev/null +++ b/tests/fixtures/cksum/untagged/blake3_single_file.expected @@ -0,0 +1 @@ +fdcf5036dc5c50e51069bb5b405345ed50b88a24da66c6f8b21b46477cf699d9 lorem_ipsum.txt diff --git a/tests/fixtures/cksum/untagged/blake3_stdin.expected b/tests/fixtures/cksum/untagged/blake3_stdin.expected new file mode 100644 index 00000000000..4da4171eec6 --- /dev/null +++ b/tests/fixtures/cksum/untagged/blake3_stdin.expected @@ -0,0 +1 @@ +fdcf5036dc5c50e51069bb5b405345ed50b88a24da66c6f8b21b46477cf699d9 - From 5d936a2b4bee85f7149f08d255a75bc60397748a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dorian=20P=C3=A9ron?= Date: Sat, 21 Mar 2026 03:51:38 +0100 Subject: [PATCH 062/264] test(cksum): add error case checks for blake2b --- tests/by-util/test_cksum.rs | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/tests/by-util/test_cksum.rs b/tests/by-util/test_cksum.rs index 666ce4cc616..2c6fe14abd9 100644 --- a/tests/by-util/test_cksum.rs +++ b/tests/by-util/test_cksum.rs @@ -968,7 +968,7 @@ fn test_dev_null() { #[cfg(unix)] #[test] -fn test_blake2b_512() { +fn test_blake2b_default_length() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; @@ -2055,6 +2055,28 @@ fn test_check_blake_length_guess() { .arg(at.subdir.join("foo.sums")) .fails() .stderr_contains("foo.sums: no properly formatted checksum lines found"); + + // This is incorrect because the length hint provided doesn't match the + // checksum length. + let length_mismatch = "BLAKE2b-8 (foo.dat) = 171cdfdf84ed"; + at.write("foo.sums", length_mismatch); + scene + .ucmd() + .arg("--check") + .arg(at.subdir.join("foo.sums")) + .fails() + .stderr_contains("foo.sums: no properly formatted checksum lines found"); + + // In this case, validation should fail because even though the checksum + // length matches the hint, it is above BLAKE2b's max (512). + let too_long = "BLAKE2b-520 (/dev/null) = 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; + at.write("foo.sums", too_long); + scene + .ucmd() + .arg("--check") + .arg(at.subdir.join("foo.sums")) + .fails() + .stderr_contains("foo.sums: no properly formatted checksum lines found"); } #[test] From e33d461b5823a456aae65c4d10b58117a6467eba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dorian=20P=C3=A9ron?= Date: Sat, 21 Mar 2026 15:02:31 +0100 Subject: [PATCH 063/264] test(cksum): Factorized sha2/sha3 tests --- tests/by-util/test_cksum.rs | 351 ++++++++++++------------------------ 1 file changed, 115 insertions(+), 236 deletions(-) diff --git a/tests/by-util/test_cksum.rs b/tests/by-util/test_cksum.rs index 2c6fe14abd9..16f18706f68 100644 --- a/tests/by-util/test_cksum.rs +++ b/tests/by-util/test_cksum.rs @@ -10,11 +10,10 @@ use rstest_reuse::{apply, template}; use uutests::at_and_ucmd; use uutests::new_ucmd; use uutests::util::TestScenario; +use uutests::util::UCommand; use uutests::util::log_info; use uutests::util_name; -const SHA_LENGTHS: [u32; 4] = [224, 256, 384, 512]; - #[template] #[rstest] #[case::sysv("sysv")] @@ -31,6 +30,18 @@ const SHA_LENGTHS: [u32; 4] = [224, 256, 384, 512]; #[case::sm3("sm3")] fn test_all_algos(#[case] algo: &str) {} +#[template] +#[rstest] +fn test_sha(#[values("sha2", "sha3")] algo: &str, #[values(224, 256, 384, 512)] len: u32) {} + +fn sha_fixture_name(algo: &str, len: u32, prefix: &str, suffix: &str) -> String { + // assume algo is always "sha2" or "sha3" + match algo { + "sha2" => format!("{prefix}sha{len}{suffix}"), + _ => format!("{prefix}sha3_{len}{suffix}"), + } +} + #[test] fn test_invalid_arg() { new_ucmd!().arg("--definitely-invalid").fails_with_code(1); @@ -375,131 +386,126 @@ fn test_sha_missing_length() { } } -#[test] -fn test_sha2_single_file() { - for l in SHA_LENGTHS { - new_ucmd!() - .arg("--algorithm=sha2") - .arg(format!("--length={l}")) - .arg("lorem_ipsum.txt") - .succeeds() - .stdout_is_fixture(format!("sha{l}_single_file.expected")); - } +fn sha_cmd(algo: &str, len: u32) -> UCommand { + let mut ucmd = new_ucmd!(); + ucmd.arg(format!("--algorithm={algo}")) + .arg(format!("--length={len}")); + ucmd } -#[test] -fn test_sha2_multiple_files() { - for l in SHA_LENGTHS { - new_ucmd!() - .arg("--algorithm=sha2") - .arg(format!("--length={l}")) - .arg("lorem_ipsum.txt") - .arg("alice_in_wonderland.txt") - .succeeds() - .stdout_is_fixture(format!("sha{l}_multiple_files.expected")); - } +#[apply(test_sha)] +fn test_sha_single_file(algo: &str, len: u32) { + sha_cmd(algo, len) + .arg("lorem_ipsum.txt") + .succeeds() + .stdout_is_fixture(sha_fixture_name(algo, len, "", "_single_file.expected")); } -#[test] -fn test_sha2_stdin() { - for l in SHA_LENGTHS { - new_ucmd!() - .arg("--algorithm=sha2") - .arg(format!("--length={l}")) - .pipe_in_fixture("lorem_ipsum.txt") - .succeeds() - .stdout_is_fixture(format!("sha{l}_stdin.expected")); - } +#[apply(test_sha)] +fn test_sha_multiple_files(algo: &str, len: u32) { + sha_cmd(algo, len) + .arg("lorem_ipsum.txt") + .arg("alice_in_wonderland.txt") + .succeeds() + .stdout_is_fixture(sha_fixture_name(algo, len, "", "_multiple_files.expected")); } -#[test] -fn test_untagged_sha2_single_file() { - for l in SHA_LENGTHS { - new_ucmd!() - .arg("--untagged") - .arg("--algorithm=sha2") - .arg(format!("--length={l}")) - .arg("lorem_ipsum.txt") - .succeeds() - .stdout_is_fixture(format!("untagged/sha{l}_single_file.expected")); - } +#[apply(test_sha)] +fn test_sha_stdin(algo: &str, len: u32) { + sha_cmd(algo, len) + .pipe_in_fixture("lorem_ipsum.txt") + .succeeds() + .stdout_is_fixture(sha_fixture_name(algo, len, "", "_stdin.expected")); } -#[test] -fn test_untagged_sha2_multiple_files() { - for l in SHA_LENGTHS { - new_ucmd!() - .arg("--untagged") - .arg("--algorithm=sha2") - .arg(format!("--length={l}")) - .arg("lorem_ipsum.txt") - .arg("alice_in_wonderland.txt") - .succeeds() - .stdout_is_fixture(format!("untagged/sha{l}_multiple_files.expected")); - } +#[apply(test_sha)] +fn test_untagged_sha_single_file(algo: &str, len: u32) { + sha_cmd(algo, len) + .arg("--untagged") + .arg("lorem_ipsum.txt") + .succeeds() + .stdout_is_fixture(sha_fixture_name( + algo, + len, + "untagged/", + "_single_file.expected", + )); } -#[test] -fn test_untagged_sha2_stdin() { - for l in SHA_LENGTHS { - new_ucmd!() - .arg("--untagged") - .arg("--algorithm=sha2") - .arg(format!("--length={l}")) - .pipe_in_fixture("lorem_ipsum.txt") - .succeeds() - .stdout_is_fixture(format!("untagged/sha{l}_stdin.expected")); - } +#[apply(test_sha)] +fn test_untagged_sha_multiple_files(algo: &str, len: u32) { + sha_cmd(algo, len) + .arg("--untagged") + .arg("lorem_ipsum.txt") + .arg("alice_in_wonderland.txt") + .succeeds() + .stdout_is_fixture(sha_fixture_name( + algo, + len, + "untagged/", + "_multiple_files.expected", + )); } -#[test] -fn test_check_tagged_sha2_single_file() { - for l in SHA_LENGTHS { - new_ucmd!() - .arg("--check") - .arg(format!("sha{l}_single_file.expected")) - .succeeds() - .stdout_is("lorem_ipsum.txt: OK\n"); - } +#[apply(test_sha)] +fn test_untagged_sha_stdin(algo: &str, len: u32) { + sha_cmd(algo, len) + .arg("--untagged") + .pipe_in_fixture("lorem_ipsum.txt") + .succeeds() + .stdout_is_fixture(sha_fixture_name(algo, len, "untagged/", "_stdin.expected")); } -#[test] -fn test_check_tagged_sha2_multiple_files() { - for l in SHA_LENGTHS { - new_ucmd!() - .arg("--check") - .arg(format!("sha{l}_multiple_files.expected")) - .succeeds() - .stdout_contains("lorem_ipsum.txt: OK\n") - .stdout_contains("alice_in_wonderland.txt: OK\n"); - } +#[apply(test_sha)] +fn test_check_tagged_sha_single_file(algo: &str, len: u32) { + new_ucmd!() + .arg("--check") + .arg(sha_fixture_name(algo, len, "", "_single_file.expected")) + .succeeds() + .stdout_is("lorem_ipsum.txt: OK\n"); } -// When checking sha2 in untagged mode, the length is automatically deduced -// from the length of the digest. -#[test] -fn test_check_untagged_sha2_single_file() { - for l in SHA_LENGTHS { - new_ucmd!() - .arg("--check") - .arg("--algorithm=sha2") - .arg(format!("untagged/sha{l}_single_file.expected")) - .succeeds() - .stdout_is("lorem_ipsum.txt: OK\n"); - } +#[apply(test_sha)] +fn test_check_tagged_sha_multiple_files(algo: &str, len: u32) { + new_ucmd!() + .arg("--check") + .arg(sha_fixture_name(algo, len, "", "_multiple_files.expected")) + .succeeds() + .stdout_contains("lorem_ipsum.txt: OK\n") + .stdout_contains("alice_in_wonderland.txt: OK\n"); } -#[test] -fn test_check_untagged_sha2_multiple_files() { - for l in SHA_LENGTHS { - new_ucmd!() - .arg("--check") - .arg("--algorithm=sha2") - .arg(format!("untagged/sha{l}_multiple_files.expected")) - .succeeds() - .stdout_contains("lorem_ipsum.txt: OK\n") - .stdout_contains("alice_in_wonderland.txt: OK\n"); - } +// When checking sha2/sha3 in untagged mode, the length is automatically +// deduced from the length of the digest. +#[apply(test_sha)] +fn test_check_untagged_sha_single_file(algo: &str, len: u32) { + new_ucmd!() + .arg("--check") + .arg(format!("--algorithm={algo}")) + .arg(sha_fixture_name( + algo, + len, + "untagged/", + "_single_file.expected", + )) + .succeeds() + .stdout_is("lorem_ipsum.txt: OK\n"); +} + +#[apply(test_sha)] +fn test_check_untagged_sha_multiple_files(algo: &str, len: u32) { + new_ucmd!() + .arg("--check") + .arg(format!("--algorithm={algo}")) + .arg(sha_fixture_name( + algo, + len, + "untagged/", + "_multiple_files.expected", + )) + .succeeds() + .stdout_contains("lorem_ipsum.txt: OK\n") + .stdout_contains("alice_in_wonderland.txt: OK\n"); } #[test] @@ -556,133 +562,6 @@ fn test_check_sha2_tagged_variant() { } } -#[test] -fn test_sha3_single_file() { - for l in SHA_LENGTHS { - new_ucmd!() - .arg("--algorithm=sha3") - .arg(format!("--length={l}")) - .arg("lorem_ipsum.txt") - .succeeds() - .stdout_is_fixture(format!("sha3_{l}_single_file.expected")); - } -} - -#[test] -fn test_sha3_multiple_files() { - for l in SHA_LENGTHS { - new_ucmd!() - .arg("--algorithm=sha3") - .arg(format!("--length={l}")) - .arg("lorem_ipsum.txt") - .arg("alice_in_wonderland.txt") - .succeeds() - .stdout_is_fixture(format!("sha3_{l}_multiple_files.expected")); - } -} - -#[test] -fn test_sha3_stdin() { - for l in SHA_LENGTHS { - new_ucmd!() - .arg("--algorithm=sha3") - .arg(format!("--length={l}")) - .pipe_in_fixture("lorem_ipsum.txt") - .succeeds() - .stdout_is_fixture(format!("sha3_{l}_stdin.expected")); - } -} - -#[test] -fn test_untagged_sha3_single_file() { - for l in SHA_LENGTHS { - new_ucmd!() - .arg("--untagged") - .arg("--algorithm=sha3") - .arg(format!("--length={l}")) - .arg("lorem_ipsum.txt") - .succeeds() - .stdout_is_fixture(format!("untagged/sha3_{l}_single_file.expected")); - } -} - -#[test] -fn test_untagged_sha3_multiple_files() { - for l in SHA_LENGTHS { - new_ucmd!() - .arg("--untagged") - .arg("--algorithm=sha3") - .arg(format!("--length={l}")) - .arg("lorem_ipsum.txt") - .arg("alice_in_wonderland.txt") - .succeeds() - .stdout_is_fixture(format!("untagged/sha3_{l}_multiple_files.expected")); - } -} - -#[test] -fn test_untagged_sha3_stdin() { - for l in SHA_LENGTHS { - new_ucmd!() - .arg("--untagged") - .arg("--algorithm=sha3") - .arg(format!("--length={l}")) - .pipe_in_fixture("lorem_ipsum.txt") - .succeeds() - .stdout_is_fixture(format!("untagged/sha3_{l}_stdin.expected")); - } -} - -#[test] -fn test_check_tagged_sha3_single_file() { - for l in SHA_LENGTHS { - new_ucmd!() - .arg("--check") - .arg(format!("sha3_{l}_single_file.expected")) - .succeeds() - .stdout_is("lorem_ipsum.txt: OK\n"); - } -} - -#[test] -fn test_check_tagged_sha3_multiple_files() { - for l in SHA_LENGTHS { - new_ucmd!() - .arg("--check") - .arg(format!("sha3_{l}_multiple_files.expected")) - .succeeds() - .stdout_contains("lorem_ipsum.txt: OK\n") - .stdout_contains("alice_in_wonderland.txt: OK\n"); - } -} - -// When checking sha3 in untagged mode, the length is automatically deduced -// from the length of the digest. -#[test] -fn test_check_untagged_sha3_single_file() { - for l in SHA_LENGTHS { - new_ucmd!() - .arg("--check") - .arg("--algorithm=sha3") - .arg(format!("untagged/sha3_{l}_single_file.expected")) - .succeeds() - .stdout_is("lorem_ipsum.txt: OK\n"); - } -} - -#[test] -fn test_check_untagged_sha3_multiple_files() { - for l in SHA_LENGTHS { - new_ucmd!() - .arg("--check") - .arg("--algorithm=sha3") - .arg(format!("untagged/sha3_{l}_multiple_files.expected")) - .succeeds() - .stdout_contains("lorem_ipsum.txt: OK\n") - .stdout_contains("alice_in_wonderland.txt: OK\n"); - } -} - #[test] fn test_check_algo() { for algo in ["bsd", "sysv", "crc", "crc32b"] { From 73f65961eab7322d764e2196aef4196c9b9f8c81 Mon Sep 17 00:00:00 2001 From: "Guillem L. Jara" <4lon3ly0@tutanota.com> Date: Wed, 25 Mar 2026 22:08:53 +0100 Subject: [PATCH 064/264] CI: add GNU tests/rm/isatty to intermittent (#11486) As seen in #11465, among others. --- .github/workflows/ignore-intermittent.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ignore-intermittent.txt b/.github/workflows/ignore-intermittent.txt index 019b3d86ed6..1ccd775b3e3 100644 --- a/.github/workflows/ignore-intermittent.txt +++ b/.github/workflows/ignore-intermittent.txt @@ -13,3 +13,4 @@ tests/misc/stdbuf tests/misc/usage_vs_getopt tests/misc/tee tests/tail/follow-name +tests/rm/isatty From fa07edb281bfcb23013b81f9723f7696919664c0 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Thu, 26 Mar 2026 06:23:21 +0900 Subject: [PATCH 065/264] stdbuf: detect unsupported target at build time (#11476) --- src/uu/stdbuf/locales/en-US.ftl | 1 - src/uu/stdbuf/locales/fr-FR.ftl | 1 - src/uu/stdbuf/src/stdbuf.rs | 18 ++---------------- 3 files changed, 2 insertions(+), 18 deletions(-) diff --git a/src/uu/stdbuf/locales/en-US.ftl b/src/uu/stdbuf/locales/en-US.ftl index f2f845a07b6..50269efc3ee 100644 --- a/src/uu/stdbuf/locales/en-US.ftl +++ b/src/uu/stdbuf/locales/en-US.ftl @@ -23,7 +23,6 @@ stdbuf-value-mode = MODE stdbuf-error-line-buffering-stdin-meaningless = line buffering stdin is meaningless stdbuf-error-invalid-mode = invalid mode {$error} stdbuf-error-value-too-large = invalid mode '{$value}': Value too large for defined data type -stdbuf-error-command-not-supported = Command not supported for this operating system! stdbuf-error-external-libstdbuf-not-found = External libstdbuf not found at configured path: {$path} stdbuf-error-permission-denied = failed to execute process: Permission denied stdbuf-error-no-such-file = failed to execute process: No such file or directory diff --git a/src/uu/stdbuf/locales/fr-FR.ftl b/src/uu/stdbuf/locales/fr-FR.ftl index 5016c6914f1..b371361aec2 100644 --- a/src/uu/stdbuf/locales/fr-FR.ftl +++ b/src/uu/stdbuf/locales/fr-FR.ftl @@ -23,7 +23,6 @@ stdbuf-value-mode = MODE stdbuf-error-line-buffering-stdin-meaningless = la mise en mémoire tampon par ligne de stdin n'a pas de sens stdbuf-error-invalid-mode = mode invalide {$error} stdbuf-error-value-too-large = mode invalide '{$value}' : Valeur trop grande pour le type de données défini -stdbuf-error-command-not-supported = Commande non prise en charge pour ce système d'exploitation ! stdbuf-error-external-libstdbuf-not-found = libstdbuf externe introuvable au chemin configuré : {$path} stdbuf-error-permission-denied = échec de l'exécution du processus : Permission refusée stdbuf-error-no-such-file = échec de l'exécution du processus : Aucun fichier ou répertoire de ce type diff --git a/src/uu/stdbuf/src/stdbuf.rs b/src/uu/stdbuf/src/stdbuf.rs index 773b5ff9ccc..da0ba7eccee 100644 --- a/src/uu/stdbuf/src/stdbuf.rs +++ b/src/uu/stdbuf/src/stdbuf.rs @@ -4,6 +4,8 @@ // file that was distributed with this source code. // spell-checker:ignore (ToDO) tempdir dyld dylib optgrps libstdbuf +#[cfg(not(unix))] +compile_error!("stdbuf is not supported on the target"); use clap::{Arg, ArgAction, ArgMatches, Command}; use std::ffi::OsString; @@ -107,22 +109,6 @@ fn preload_strings() -> UResult<(&'static str, &'static str)> { Ok(("DYLD_LIBRARY_PATH", "dylib")) } -#[cfg(not(any( - target_os = "linux", - target_os = "android", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd", - target_os = "dragonfly", - target_vendor = "apple" -)))] -fn preload_strings() -> UResult<(&'static str, &'static str)> { - Err(USimpleError::new( - 1, - translate!("stdbuf-error-command-not-supported"), - )) -} - fn check_option(matches: &ArgMatches, name: &str) -> Result { match matches.get_one::(name) { Some(value) => match value.as_str() { From d8a94198ff3d6002d319384230119270e7863459 Mon Sep 17 00:00:00 2001 From: pin Date: Wed, 25 Mar 2026 22:24:17 +0100 Subject: [PATCH 066/264] Allow `feat_os_unix` on NetBSD (#11470) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Allow `feat_os_unix` on NetBSD Now that #11428 was merged, this should be fine. However, CI will still fail. See discussion on https://github.com/uutils/coreutils/issues/5297#issuecomment-4109942321 for a potential fix. Compiling `cross` from git will cost 1m 31s on a 2014 Intel® Core™ i5-4210U CPU @ 1.70GHz (4) --- .github/workflows/CICD.yml | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index 45f80915464..1a205788002 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -5,7 +5,7 @@ name: CICD # spell-checker:ignore (jargon) SHAs deps dequote softprops subshell toolchain fuzzers dedupe devel profdata # spell-checker:ignore (people) Peltoche rivy dtolnay Anson dawidd # spell-checker:ignore (shell/tools) binutils choco clippy dmake esac fakeroot fdesc fdescfs gmake grcov halium lcov libclang libfuse libssl limactl nextest nocross pacman popd printf pushd redoxer rsync rustc rustfmt rustup shopt sccache utmpdump xargs zstd -# spell-checker:ignore (misc) aarch alnum armhf bindir busytest coreutils defconfig DESTDIR gecos getenforce gnueabihf issuecomment maint manpages msys multisize noconfirm nofeatures nullglob onexitbegin onexitend pell runtest Swatinem tempfile testsuite toybox uutils libsystemd codspeed wasip +# spell-checker:ignore (misc) aarch alnum armhf bindir busytest coreutils defconfig DESTDIR gecos getenforce gnueabihf issuecomment maint manpages msys multisize noconfirm nofeatures nullglob onexitbegin onexitend pell runtest Swatinem tempfile testsuite toybox uutils libsystemd codspeed wasip libexecinfo env: PROJECT_NAME: coreutils @@ -378,7 +378,7 @@ jobs: - { os: ubuntu-latest , target: x86_64-unknown-linux-gnu , features: "feat_os_unix,test_risky_names", use-cross: use-cross, skip-publish: true } - { os: ubuntu-latest , target: x86_64-unknown-linux-gnu , features: "feat_os_unix,uudoc" , use-cross: no, workspace-tests: true } - { os: ubuntu-latest , target: x86_64-unknown-linux-musl , features: feat_os_unix_musl , use-cross: use-cross } - - { os: ubuntu-latest , target: x86_64-unknown-netbsd, features: "feat_Tier1,feat_require_unix_core,feat_require_unix_hostid,feat_require_unix_utmpx", use-cross: use-cross , skip-tests: true , check-only: true } + - { os: ubuntu-latest , target: x86_64-unknown-netbsd, features: "feat_os_unix", use-cross: use-cross , skip-tests: true , check-only: true } - { os: ubuntu-latest , target: x86_64-unknown-redox , features: feat_os_unix_redox , use-cross: redoxer , skip-tests: true , check-only: true } - { os: ubuntu-latest , target: wasm32-wasip1, default-features: false, features: feat_wasm, skip-tests: true } - { os: macos-latest , target: aarch64-apple-darwin , features: feat_os_macos, workspace-tests: true } # M1 CPU @@ -504,9 +504,20 @@ jobs: ;; esac - uses: taiki-e/install-action@v2 - if: steps.vars.outputs.CARGO_CMD == 'cross' + # `cross` v0.2.5 lacks `libexecinfo` on NetBSD. However, this has been added in main. + # See https://github.com/cross-rs/cross/blob/main/docker/netbsd.sh + # We are pulling cross from a specific commit hash rather than HEAD to avoid any potential breakage that may be introduced. + # Once `cross` v0.3 is out, these conditionals can be removed. + if: steps.vars.outputs.CARGO_CMD == 'cross' && matrix.job.target != 'x86_64-unknown-netbsd' with: tool: cross@0.2.5 + - uses: dtolnay/rust-toolchain@master + if: steps.vars.outputs.CARGO_CMD == 'cross' && matrix.job.target == 'x86_64-unknown-netbsd' + with: + toolchain: "1.92.0" + - name: Install cross from git (NetBSD) + if: steps.vars.outputs.CARGO_CMD == 'cross' && matrix.job.target == 'x86_64-unknown-netbsd' + run: cargo +1.92.0 install cross --git https://github.com/cross-rs/cross --rev 588b3c99db52b5a9c5906fab96cfadcf1bde7863 - name: Create all needed build/work directories shell: bash run: | @@ -603,7 +614,7 @@ jobs: if: matrix.job.skip-publish != true && matrix.job.check-only == true run: | # expr breaks redox - sed -i.b '/"expr",/d' Cargo.toml + if [[ "${{ matrix.job.target }}" == *"redox"* ]]; then sed -i.b '/"expr",/d' Cargo.toml; fi ${{ steps.vars.outputs.CARGO_CMD }} ${{ steps.vars.outputs.CARGO_CMD_OPTIONS }} check \ --target=${{ matrix.job.target }} ${{ matrix.job.cargo-options }} ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} ${{ steps.vars.outputs.CARGO_DEFAULT_FEATURES_OPTION }} - name: Test From 107c91249c27bb5e5cc4c9de4ca8be9f8f0cafef Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 05:10:11 +0000 Subject: [PATCH 067/264] chore(deps): update davidanson/markdownlint-cli2-action action to v23 --- .github/workflows/CICD.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index 1a205788002..fb3ca65f45c 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -152,7 +152,7 @@ jobs: shell: bash run: | RUSTDOCFLAGS="-Dwarnings" cargo doc ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} --no-deps --workspace --document-private-items - - uses: DavidAnson/markdownlint-cli2-action@v22 + - uses: DavidAnson/markdownlint-cli2-action@v23 with: fix: "true" globs: | From b91d745120170e59fdb1fe902cb4d5d98369517f Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Thu, 26 Mar 2026 18:46:33 +0900 Subject: [PATCH 068/264] hostid: simplify the code (#11502) --- src/uu/hostid/src/hostid.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/uu/hostid/src/hostid.rs b/src/uu/hostid/src/hostid.rs index 041dc232344..bad5f9a79f2 100644 --- a/src/uu/hostid/src/hostid.rs +++ b/src/uu/hostid/src/hostid.rs @@ -21,10 +21,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { * is a no-op unless unsigned int is wider than 32 bits. */ - let mut result: c_long; - unsafe { - result = gethostid(); - } + let mut result: c_long = unsafe { gethostid() }; #[allow(overflowing_literals)] let mask = 0xffff_ffff; From 0a613d279781508ba3cdb4089ff4299d72e3e257 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Thu, 26 Mar 2026 20:03:31 +0900 Subject: [PATCH 069/264] logname: reduce range of unsafe block --- src/uu/logname/src/logname.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/uu/logname/src/logname.rs b/src/uu/logname/src/logname.rs index 32b0c85105c..3badf4c1cb4 100644 --- a/src/uu/logname/src/logname.rs +++ b/src/uu/logname/src/logname.rs @@ -12,13 +12,11 @@ use uucore::translate; use uucore::{error::UResult, show_error}; fn get_userlogin() -> Option { - unsafe { - let login: *const libc::c_char = libc::getlogin(); - if login.is_null() { - None - } else { - Some(String::from_utf8_lossy(CStr::from_ptr(login).to_bytes()).to_string()) - } + let login_ptr = unsafe { libc::getlogin() }; + if login_ptr.is_null() { + None + } else { + Some(String::from_utf8_lossy(unsafe { CStr::from_ptr(login_ptr) }.to_bytes()).to_string()) } } From 34929f2650d0b83b75180eb58175c0b4061afbf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Can=20B=C3=B6l=C3=BCk?= Date: Thu, 26 Mar 2026 16:21:28 +0100 Subject: [PATCH 070/264] tr: fix graph/print character class mapping (#11405) Co-authored-by: Sylvestre Ledru --- src/uu/tr/src/operation.rs | 6 +++--- tests/by-util/test_tr.rs | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/uu/tr/src/operation.rs b/src/uu/tr/src/operation.rs index f945bebca7c..616c911c4dc 100644 --- a/src/uu/tr/src/operation.rs +++ b/src/uu/tr/src/operation.rs @@ -185,8 +185,7 @@ impl Sequence { .chain(33..=47) .chain(58..=64) .chain(91..=96) - .chain(123..=126) - .chain(std::iter::once(32)), // space + .chain(123..=126), ), Class::Print => Box::new( (48..=57) // digit @@ -196,7 +195,8 @@ impl Sequence { .chain(33..=47) .chain(58..=64) .chain(91..=96) - .chain(123..=126), + .chain(123..=126) + .chain(std::iter::once(32)), // space ), Class::Punct => Box::new((33..=47).chain(58..=64).chain(91..=96).chain(123..=126)), Class::Space => Box::new(unicode_table::SPACES.iter().copied()), diff --git a/tests/by-util/test_tr.rs b/tests/by-util/test_tr.rs index fd4fc91a7fd..37f1af97212 100644 --- a/tests/by-util/test_tr.rs +++ b/tests/by-util/test_tr.rs @@ -65,6 +65,38 @@ fn test_delete() { .stdout_is("BD"); } +#[test] +fn test_delete_graph_and_print_match_gnu() { + let input = [b' ', b'A', b'!', b'\t', b'\n']; + new_ucmd!() + .args(&["-d", "[:graph:]"]) + .pipe_in(input) + .succeeds() + .stdout_is_bytes([b' ', b'\t', b'\n']); + + new_ucmd!() + .args(&["-d", "[:print:]"]) + .pipe_in(input) + .succeeds() + .stdout_is_bytes([b'\t', b'\n']); +} + +#[test] +fn test_delete_complement_graph_and_print_match_gnu() { + let input = [b' ', b'A', b'!', b'\t', b'\n']; + new_ucmd!() + .args(&["-d", "-c", "[:graph:]"]) + .pipe_in(input) + .succeeds() + .stdout_is_bytes([b'A', b'!']); + + new_ucmd!() + .args(&["-d", "-c", "[:print:]"]) + .pipe_in(input) + .succeeds() + .stdout_is_bytes([b' ', b'A', b'!']); +} + #[test] fn test_delete_afterwards_is_not_flag() { new_ucmd!() From d1219523d014129019a1ed46c6390cde0f0bcb59 Mon Sep 17 00:00:00 2001 From: Vincent Whitchurch Date: Thu, 26 Mar 2026 10:03:03 +0000 Subject: [PATCH 071/264] sort: fix infinite loop on --files0-from read error If the read from the `--files0-from` file results in a permanent error, we end up in an infinite loop. --- src/uu/sort/src/sort.rs | 6 +++++- tests/by-util/test_sort.rs | 9 +++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/uu/sort/src/sort.rs b/src/uu/sort/src/sort.rs index c87025a509d..201267906bb 100644 --- a/src/uu/sort/src/sort.rs +++ b/src/uu/sort/src/sort.rs @@ -2042,7 +2042,11 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // sort errors with "cannot open: [...]" instead of "cannot read: [...]" here let reader = open_with_open_failed_error(&files0_from)?; let buf_reader = BufReader::new(reader); - for (line_num, line) in buf_reader.split(b'\0').flatten().enumerate() { + for (line_num, line_res) in buf_reader.split(b'\0').enumerate() { + let line = line_res.map_err(|error| SortError::ReadFailed { + path: files0_from.clone(), + error, + })?; let f = std::str::from_utf8(&line) .expect("Could not parse string from zero terminated input."); match f { diff --git a/tests/by-util/test_sort.rs b/tests/by-util/test_sort.rs index d92f6da003b..ea4abed17a0 100644 --- a/tests/by-util/test_sort.rs +++ b/tests/by-util/test_sort.rs @@ -1569,6 +1569,15 @@ fn test_files0_from_empty() { .stderr_only("sort: no input from 'file'\n"); } +#[test] +#[cfg(unix)] +fn test_files0_read_error() { + new_ucmd!() + .args(&["--files0-from", "."]) + .fails_with_code(2) + .stderr_only("sort: cannot read: .: Is a directory\n"); +} + #[cfg(target_os = "linux")] #[test] // Test for GNU tests/sort/sort-files0-from.pl "empty-non-regular" From 80fcb4470ade42474515bed81fb9950c14f98c5e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 15:23:26 +0000 Subject: [PATCH 072/264] chore(deps): update codecov/codecov-action action to v6 --- .github/workflows/CICD.yml | 10 +++++----- .github/workflows/make.yml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index fb3ca65f45c..750fb9bcccc 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -237,7 +237,7 @@ jobs: RUST_BACKTRACE: "1" - name: Upload test results to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5 + uses: codecov/codecov-action@v6 with: token: ${{ secrets.CODECOV_TOKEN }} report_type: test_results @@ -301,7 +301,7 @@ jobs: RUST_BACKTRACE: "1" - name: Upload test results to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5 + uses: codecov/codecov-action@v6 with: token: ${{ secrets.CODECOV_TOKEN }} report_type: test_results @@ -344,7 +344,7 @@ jobs: RUST_BACKTRACE: "1" - name: Upload test results to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5 + uses: codecov/codecov-action@v6 with: token: ${{ secrets.CODECOV_TOKEN }} report_type: test_results @@ -809,7 +809,7 @@ jobs: # RUSTUP_TOOLCHAIN: ${{ steps.vars.outputs.TOOLCHAIN }} - name: Upload coverage results (to Codecov.io) - uses: codecov/codecov-action@v5 + uses: codecov/codecov-action@v6 with: token: ${{ secrets.CODECOV_TOKEN }} files: ${{ steps.run_test_cov.outputs.report }} @@ -819,7 +819,7 @@ jobs: fail_ci_if_error: false - name: Upload test results to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5 + uses: codecov/codecov-action@v6 with: token: ${{ secrets.CODECOV_TOKEN }} report_type: test_results diff --git a/.github/workflows/make.yml b/.github/workflows/make.yml index fb1ed4bd9c6..5ac2d13f347 100644 --- a/.github/workflows/make.yml +++ b/.github/workflows/make.yml @@ -85,7 +85,7 @@ jobs: RUST_BACKTRACE: "1" - name: Upload test results to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5 + uses: codecov/codecov-action@v6 with: token: ${{ secrets.CODECOV_TOKEN }} report_type: test_results From 3ad4a7915607700bcaf5b3dd528257eb79c46038 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Fri, 27 Mar 2026 11:05:57 +0900 Subject: [PATCH 073/264] hostname: reduce range of unsafe --- src/uu/hostname/src/hostname.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/uu/hostname/src/hostname.rs b/src/uu/hostname/src/hostname.rs index 70c20e780f3..747f7794894 100644 --- a/src/uu/hostname/src/hostname.rs +++ b/src/uu/hostname/src/hostname.rs @@ -38,10 +38,8 @@ mod wsa { pub(super) struct WsaHandle(()); pub(super) fn start() -> io::Result { - let err = unsafe { - let mut data = std::mem::MaybeUninit::::uninit(); - WSAStartup(0x0202, data.as_mut_ptr()) - }; + let mut data = std::mem::MaybeUninit::::uninit(); + let err = unsafe { WSAStartup(0x0202, data.as_mut_ptr()) }; if err == 0 { Ok(WsaHandle(())) } else { @@ -51,10 +49,8 @@ mod wsa { impl Drop for WsaHandle { fn drop(&mut self) { - unsafe { - // This possibly returns an error but we can't handle it - let _err = WSACleanup(); - } + // This possibly returns an error but we can't handle it + let _ = unsafe { WSACleanup() }; } } } From 7d719cce9aa716c46ccf1c2500acc084e3d9ff23 Mon Sep 17 00:00:00 2001 From: oech3 <> Date: Fri, 27 Mar 2026 14:35:56 +0900 Subject: [PATCH 074/264] yes: drop unused feat deps --- src/uu/yes/Cargo.toml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/uu/yes/Cargo.toml b/src/uu/yes/Cargo.toml index b8703bb55d1..1c2c53dde43 100644 --- a/src/uu/yes/Cargo.toml +++ b/src/uu/yes/Cargo.toml @@ -22,12 +22,7 @@ path = "src/yes.rs" clap = { workspace = true } itertools = { workspace = true } fluent = { workspace = true } - -[target.'cfg(unix)'.dependencies] -uucore = { workspace = true, features = ["pipes", "signals"] } - -[target.'cfg(not(unix))'.dependencies] -uucore = { workspace = true, features = ["pipes"] } +uucore = { workspace = true } [[bin]] name = "yes" From e7e5e84aae483dd16e031a492f1399ae0741667f Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Fri, 27 Mar 2026 17:52:28 +0900 Subject: [PATCH 075/264] Remove duplicated features (#11510) --- .github/workflows/CICD.yml | 12 ++++++------ .github/workflows/code-quality.yml | 2 +- .github/workflows/l10n.yml | 6 +++--- Cargo.toml | 5 ----- README.md | 2 -- docs/src/installation.md | 4 +--- tests/by-util/test_chown.rs | 2 +- 7 files changed, 12 insertions(+), 21 deletions(-) diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index 750fb9bcccc..c07747c1b8e 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -102,7 +102,7 @@ jobs: # for now, don't build it on mac & windows because the doc is only published from linux # + it needs a bunch of duplication for build # and I don't want to add a doc step in the regular build to avoid long builds -# - { os: macos-latest , features: feat_os_macos } +# - { os: macos-latest , features: feat_os_unix } # - { os: windows-latest , features: feat_os_windows } steps: - uses: actions/checkout@v6 @@ -277,7 +277,7 @@ jobs: matrix: job: - { os: ubuntu-latest , features: feat_os_unix } - - { os: macos-latest , features: feat_os_macos } + - { os: macos-latest , features: feat_os_unix } - { os: windows-latest , features: feat_os_windows } steps: - uses: actions/checkout@v6 @@ -320,7 +320,7 @@ jobs: matrix: job: - { os: ubuntu-latest , features: feat_os_unix } - - { os: macos-latest , features: feat_os_macos } + - { os: macos-latest , features: feat_os_unix } - { os: windows-latest , features: feat_os_windows } steps: - uses: actions/checkout@v6 @@ -381,10 +381,10 @@ jobs: - { os: ubuntu-latest , target: x86_64-unknown-netbsd, features: "feat_os_unix", use-cross: use-cross , skip-tests: true , check-only: true } - { os: ubuntu-latest , target: x86_64-unknown-redox , features: feat_os_unix_redox , use-cross: redoxer , skip-tests: true , check-only: true } - { os: ubuntu-latest , target: wasm32-wasip1, default-features: false, features: feat_wasm, skip-tests: true } - - { os: macos-latest , target: aarch64-apple-darwin , features: feat_os_macos, workspace-tests: true } # M1 CPU + - { os: macos-latest , target: aarch64-apple-darwin , features: feat_os_unix, workspace-tests: true } # M1 CPU # PR #7964: chcon should not break build without the feature. cargo check is enough to detect it. - { os: macos-latest , target: aarch64-apple-darwin , workspace-tests: true, check-only: true } # M1 CPU - - { os: macos-latest , target: x86_64-apple-darwin , features: feat_os_macos, workspace-tests: true } + - { os: macos-latest , target: x86_64-apple-darwin , features: feat_os_unix, workspace-tests: true } - { os: windows-latest , target: i686-pc-windows-msvc , features: feat_os_windows } - { os: windows-latest , target: x86_64-pc-windows-gnu , features: feat_os_windows } - { os: windows-latest , target: x86_64-pc-windows-msvc , features: feat_os_windows } @@ -870,7 +870,7 @@ jobs: fail-fast: false matrix: job: - - { os: macos-latest , features: feat_os_macos } + - { os: macos-latest , features: feat_os_unix } - { os: windows-latest , features: feat_os_windows } steps: diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index e5f47562d6a..7249ce37b6d 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -72,7 +72,7 @@ jobs: matrix: job: - { os: ubuntu-latest , features: all , workspace: true } - - { os: macos-latest , features: feat_os_macos } + - { os: macos-latest , features: feat_os_unix } - { os: windows-latest , features: feat_os_windows } steps: - uses: actions/checkout@v6 diff --git a/.github/workflows/l10n.yml b/.github/workflows/l10n.yml index 30b2cd5b17e..98504f99778 100644 --- a/.github/workflows/l10n.yml +++ b/.github/workflows/l10n.yml @@ -31,7 +31,7 @@ jobs: matrix: job: - { os: ubuntu-latest , features: "feat_os_unix" } - - { os: macos-latest , features: "feat_os_macos" } + - { os: macos-latest , features: "feat_os_unix" } - { os: windows-latest , features: "feat_os_windows" } steps: - uses: actions/checkout@v6 @@ -397,7 +397,7 @@ jobs: matrix: job: - { os: ubuntu-latest , features: "feat_os_unix" } - - { os: macos-latest , features: "feat_os_macos" } + - { os: macos-latest , features: "feat_os_unix" } steps: - uses: actions/checkout@v6 with: @@ -550,7 +550,7 @@ jobs: matrix: job: - { os: ubuntu-latest , features: "feat_os_unix" } - - { os: macos-latest , features: "feat_os_macos" } + - { os: macos-latest , features: "feat_os_unix" } steps: - uses: actions/checkout@v6 with: diff --git a/Cargo.toml b/Cargo.toml index db9c0c40523..6a816387454 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,6 @@ all-features = true [features] default = ["feat_common_core"] ## OS feature shortcodes -macos = ["feat_os_macos"] unix = ["feat_os_unix"] windows = ["feat_os_windows"] ## project-specific feature shortcodes @@ -230,10 +229,6 @@ feat_wasm = [ "arch", "uname", ] -# "feat_os_macos" == set of utilities which can be built/run on the MacOS platform -feat_os_macos = [ - "feat_os_unix", ## == a modern/usual *nix platform -] # "feat_os_unix" == set of utilities which can be built/run on modern/usual *nix platforms. feat_os_unix = [ "feat_Tier1", diff --git a/README.md b/README.md index 6708d9dcadc..007f19cdd89 100644 --- a/README.md +++ b/README.md @@ -111,8 +111,6 @@ sets of uutils for a platform (on that platform) is as simple as specifying it as a feature: ```shell -cargo build --release --features macos -# or ... cargo build --release --features windows # or ... cargo build --release --features unix diff --git a/docs/src/installation.md b/docs/src/installation.md index 8ff0f004efd..a62b6fa8ab4 100644 --- a/docs/src/installation.md +++ b/docs/src/installation.md @@ -15,10 +15,8 @@ You can also [build uutils from source](build.md). [![crates.io package](https://repology.org/badge/version-for-repo/crates_io/uutils-coreutils.svg)](https://crates.io/crates/coreutils) ```shell -# Linux +# Unix like cargo install coreutils --features unix --locked -# MacOs -cargo install coreutils --features macos --locked # Windows cargo install coreutils --features windows --locked ``` diff --git a/tests/by-util/test_chown.rs b/tests/by-util/test_chown.rs index 2602aabde06..14b7f84d859 100644 --- a/tests/by-util/test_chown.rs +++ b/tests/by-util/test_chown.rs @@ -494,7 +494,7 @@ fn test_chown_only_group_id() { } result.stderr_contains("retained as"); - // Apparently on CI "macos-latest, x86_64-apple-darwin, feat_os_macos" + // Apparently on CI "macos-latest, x86_64-apple-darwin, feat_os_unix" // the process has the rights to change from runner:staff to runner:wheel #[cfg(any(windows, all(unix, not(target_os = "macos"))))] // FreeBSD user on CI is part of wheel group From 9d4faf80891322e8697ec302c7e490551e68f326 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Fri, 27 Mar 2026 20:54:09 +0900 Subject: [PATCH 076/264] who: reduce range of unsafe (#11521) --- src/uu/who/src/platform/unix.rs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/uu/who/src/platform/unix.rs b/src/uu/who/src/platform/unix.rs index a91f12fdeb8..6fb70753dd0 100644 --- a/src/uu/who/src/platform/unix.rs +++ b/src/uu/who/src/platform/unix.rs @@ -179,16 +179,14 @@ fn time_string(ut: &UtmpxRecord) -> String { #[inline] fn current_tty() -> String { - unsafe { - let res = ttyname(STDIN_FILENO); - if res.is_null() { - String::new() - } else { - CStr::from_ptr(res.cast_const()) - .to_string_lossy() - .trim_start_matches("/dev/") - .to_owned() - } + let p = unsafe { ttyname(STDIN_FILENO) }; + if p.is_null() { + String::new() + } else { + unsafe { CStr::from_ptr(p) } + .to_string_lossy() + .trim_start_matches("/dev/") + .to_owned() } } From b187b25a772dbd3e5c3cb6d40caddbce0beca20f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Can=20B=C3=B6l=C3=BCk?= Date: Fri, 27 Mar 2026 15:05:01 +0100 Subject: [PATCH 077/264] head: reset stale partial line state on reused buffers (#11407) Co-authored-by: Sylvestre Ledru --- src/uu/head/src/take.rs | 1 + tests/by-util/test_head.rs | 29 +++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/uu/head/src/take.rs b/src/uu/head/src/take.rs index 6f05b77e595..b557036579f 100644 --- a/src/uu/head/src/take.rs +++ b/src/uu/head/src/take.rs @@ -163,6 +163,7 @@ impl TakeAllLinesBuffer { reader: &mut impl Read, separator: u8, ) -> std::io::Result { + self.partial_line = false; let bytes_read = self.inner.fill_buffer(reader)?; // Count the number of lines... self.terminated_lines = memchr_iter(separator, self.inner.remaining_buffer()).count(); diff --git a/tests/by-util/test_head.rs b/tests/by-util/test_head.rs index 2acc783eaff..e35d347f3b3 100644 --- a/tests/by-util/test_head.rs +++ b/tests/by-util/test_head.rs @@ -451,6 +451,35 @@ fn test_all_but_last_bytes_large_file_piped() { .stdout_only_fixture(seq_19000_file_name); } +#[test] +fn test_all_but_last_lines_large_file_presume_input_pipe() { + // Validate print-all-but-last-n-lines on the non-seekable path for a large terminated input. + // This input shape forces repeated buffer reuse while some chunks end mid-line. + let scene = TestScenario::new(util_name!()); + let fixtures = &scene.fixtures; + + let input_file_name = "reused_line_chunks"; + let expected_output_file_name = "reused_line_chunks_elide_last"; + let line = "aaaaaa\n"; + let input_line_count: usize = 20_000; + + let mut input = String::with_capacity(line.len() * input_line_count); + for _ in 0..input_line_count { + input.push_str(line); + } + fixtures.write(input_file_name, &input); + fixtures.write( + expected_output_file_name, + &line.repeat(input_line_count - 1), + ); + + scene + .ucmd() + .args(&["---presume-input-pipe", "-n", "-1", input_file_name]) + .succeeds() + .stdout_only_fixture(expected_output_file_name); +} + #[test] fn test_all_but_last_lines_large_file() { // Create our fixtures on the fly. We need the input file to be at least double From 1fbed8690aa7780bda5bd7bad8f61f3df22ba2a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dorian=20P=C3=A9ron?= Date: Wed, 25 Mar 2026 23:16:42 +0100 Subject: [PATCH 078/264] test(cksum): Add multiple tests on check length digest validation --- tests/by-util/test_cksum.rs | 110 ++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/tests/by-util/test_cksum.rs b/tests/by-util/test_cksum.rs index 16f18706f68..16ed6d8b373 100644 --- a/tests/by-util/test_cksum.rs +++ b/tests/by-util/test_cksum.rs @@ -508,6 +508,49 @@ fn test_check_untagged_sha_multiple_files(algo: &str, len: u32) { .stdout_contains("alice_in_wonderland.txt: OK\n"); } +#[rstest] +#[case::sha2("sha2", &[ + "d14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b", + "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e" +])] +#[case::sha3("sha3", &[ + "6b4e03423667dbb73b6e15454f0eb1abd4597f9a1b078e3f5b5a6bc7", + "a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a", + "0c63a75b845e4f7d01107d852e4c2485c51a50aaaa94fc61995e71bbee983a2ac3713831264adb47fb6bd1e058d5f004", + "a69f73cca23a9ac5c8b567dc185a756e97c982164fe25859e0d1dcc1475c80a615b2123af1f5f94c11e3e9402c3ac558f500199d95b6d3e301758586281dcd26" +])] +fn test_check_untagged_sha_invalid_length(#[case] algo: &str, #[case] digests: &[&str; 4]) { + // When checking with --algorithm sha2/sha3, Guess the length from the provided + // digest. Raise "improperly formatted" if the digest is not af adequate + // size (224, 256, 384 or 512 bits). + + let (at, mut ucmd) = at_and_ucmd!(); + at.touch("a"); + at.touch("b"); + at.touch("c"); + at.touch("d"); + at.touch("e"); + + let invalid = "xxxx e"; + + ucmd.arg("-a") + .arg(algo) + .arg("-c") + .pipe_in(format!( + "{} a\n{} b\n{} c\n{} d\n{invalid}", + digests[0], digests[1], digests[2], digests[3] + )) + .succeeds() + .stdout_contains("a: OK") + .stdout_contains("b: OK") + .stdout_contains("c: OK") + .stdout_contains("d: OK") + .stdout_does_not_contain("e: FAILED") + .stderr_contains("improperly formatted"); +} + #[test] fn test_check_sha2_tagged_variant() { let scene = TestScenario::new(util_name!()); @@ -562,6 +605,73 @@ fn test_check_sha2_tagged_variant() { } } +#[test] +fn test_check_sha2_tagged_missing_hint() { + // SHA2 () = + // + // should fail and raise "improperly formatted" because SHA2 expects a + // mandatory length hint. + + let (at, mut ucmd) = at_and_ucmd!(); + at.touch("a"); + at.touch("b"); + + // valid digest but missing hint + let invalid_sha224 = "SHA2 (a) = d14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f"; + // invalid digest + let invalid = "SHA2 (b) = xxxx"; + + ucmd.arg("-c") + .pipe_in(format!("{invalid_sha224}\n{invalid}")) + .fails() + .stderr_contains("no properly formatted checksum lines found"); +} + +#[rstest] +#[case::md5("md5", "d41d8cd98f00b204e9800998ecf8427e")] +#[case::sha1("sha1", "da39a3ee5e6b4b0d3255bfef95601890afd80709")] +#[case::sha224("sha224", "d14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f")] +#[case::sha256( + "sha256", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" +)] +#[case::sha384( + "sha384", + "38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b" +)] +#[case::sha512( + "sha512", + "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e" +)] +#[case::sm3( + "sm3", + "1ab21d8355cfa17f8e61194831e81a8f22bec8c728fefb747ed035eb5082aa2b" +)] +fn test_check_untagged_with_invalid_length(#[case] algo: &str, #[case] digest: &str) { + // issue #11202 + + // Ensures that when checking untagged lines, if the provided algorithm is + // one of `sha(224|256|384|512)`, digests whose length mismatches the + // length of the given algorithm will report as "improperly formatted" + // rather than a FAILED check. + + let (at, mut ucmd) = at_and_ucmd!(); + at.touch("a"); + at.touch("b"); + + let good_checksum = format!("{digest} a"); + let invalid_checksum = "e3b0 b"; + + ucmd.arg("-a") + .arg(algo) + .arg("-c") + .pipe_in(format!("{invalid_checksum}\n{good_checksum}")) + .succeeds() + .stdout_contains("a: OK") + .stdout_does_not_contain("b: FAILED") + .stderr_contains("WARNING: 1 line is improperly formatted"); +} + #[test] fn test_check_algo() { for algo in ["bsd", "sysv", "crc", "crc32b"] { From 61a02386717dbadc6d35e98526bac9616911a920 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dorian=20P=C3=A9ron?= Date: Thu, 26 Mar 2026 00:18:10 +0100 Subject: [PATCH 079/264] cksum: make sure --check raises "improperly formatted" for length-guessing related issues cksum: Fix --check failing when digest length is wrong on untagged line --- src/uucore/src/lib/features/checksum/mod.rs | 16 +++++++++ .../src/lib/features/checksum/validate.rs | 34 ++++++++++++------- src/uucore/src/lib/features/sum.rs | 11 ++++-- 3 files changed, 46 insertions(+), 15 deletions(-) diff --git a/src/uucore/src/lib/features/checksum/mod.rs b/src/uucore/src/lib/features/checksum/mod.rs index e6145d3748e..d18e2adaad8 100644 --- a/src/uucore/src/lib/features/checksum/mod.rs +++ b/src/uucore/src/lib/features/checksum/mod.rs @@ -200,6 +200,22 @@ impl AlgoKind { use AlgoKind::*; matches!(self, Sysv | Bsd | Crc | Crc32b) } + + /// When checking untagged format lines, non-XOF non-legacy algorithms + /// should report "improperly formatted lines" if the digest length isn't + /// equivalent to this. + pub fn expected_digest_bit_len(self) -> Option { + match self { + Self::Md5 => Some(Md5::BIT_SIZE), + Self::Sm3 => Some(Sm3::BIT_SIZE), + Self::Sha1 => Some(Sha1::BIT_SIZE), + Self::Sha224 => Some(Sha224::BIT_SIZE), + Self::Sha256 => Some(Sha256::BIT_SIZE), + Self::Sha384 => Some(Sha384::BIT_SIZE), + Self::Sha512 => Some(Sha512::BIT_SIZE), + _ => None, + } + } } /// Holds a length for a SHA2 of SHA3 algorithm kind. diff --git a/src/uucore/src/lib/features/checksum/validate.rs b/src/uucore/src/lib/features/checksum/validate.rs index fa51539fe1e..bb47ff6c920 100644 --- a/src/uucore/src/lib/features/checksum/validate.rs +++ b/src/uucore/src/lib/features/checksum/validate.rs @@ -15,7 +15,7 @@ use std::io::{self, BufReader, Read, Write, stderr, stdin}; use os_display::Quotable; use crate::checksum::{ - AlgoKind, BlakeLength, ChecksumError, ReadingMode, SizedAlgoKind, digest_reader, + AlgoKind, BlakeLength, ChecksumError, ReadingMode, ShaLength, SizedAlgoKind, digest_reader, parse_blake_length, unescape_filename, }; use crate::error::{FromIo, UError, UIoError, UResult, USimpleError}; @@ -490,7 +490,7 @@ impl LineInfo { } /// Extract the expected digest from the checksum string and decode it -fn get_raw_expected_digest(checksum: &str, byte_len_hint: Option) -> Option> { +fn get_raw_expected_digest(checksum: &str, bit_len_hint: Option) -> Option> { // If the length of the digest is not a multiple of 2, then it must be // improperly formatted (1 byte is 2 hex digits, and base64 strings should // always be a multiple of 4). @@ -498,6 +498,8 @@ fn get_raw_expected_digest(checksum: &str, byte_len_hint: Option) -> Opti return None; } + let byte_len_hint = bit_len_hint.map(|n| n.div_ceil(8)); + let checks_hint = |len| byte_len_hint.is_none_or(|hint| hint == len); // If the length of the string matches the one to be expected (in case it's @@ -741,23 +743,23 @@ fn process_algo_based_line( ) -> Result<(), LineCheckError> { let filename_to_check = line_info.filename.as_slice(); - let (algo_kind, algo_byte_len) = - identify_algo_name_and_length(line_info, cli_algo_kind, last_algo)?; + let (algo_kind, algo_len) = identify_algo_name_and_length(line_info, cli_algo_kind, last_algo)?; // If the digest bitlen is known, we can check the format of the expected // checksum with it. - let digest_char_length_hint = match (algo_kind, algo_byte_len) { - (AlgoKind::Blake2b | AlgoKind::Blake3, Some(byte_len)) => Some(byte_len), - (AlgoKind::Shake128 | AlgoKind::Shake256, Some(bit_len)) => Some(bit_len.div_ceil(8)), - (AlgoKind::Shake128, None) => Some(sum::Shake128::DEFAULT_BIT_SIZE.div_ceil(8)), - (AlgoKind::Shake256, None) => Some(sum::Shake256::DEFAULT_BIT_SIZE.div_ceil(8)), + let digest_bit_length_hint = match (algo_kind, algo_len) { + (AlgoKind::Blake2b | AlgoKind::Blake3, Some(byte_len)) => Some(byte_len * 8), + (AlgoKind::Shake128 | AlgoKind::Shake256, Some(bit_len)) => Some(bit_len), + (AlgoKind::Shake128, None) => Some(sum::Shake128::DEFAULT_BIT_SIZE), + (AlgoKind::Shake256, None) => Some(sum::Shake256::DEFAULT_BIT_SIZE), _ => None, }; - let expected_checksum = get_raw_expected_digest(&line_info.checksum, digest_char_length_hint) + let expected_checksum = get_raw_expected_digest(&line_info.checksum, digest_bit_length_hint) .ok_or(LineCheckError::ImproperlyFormatted)?; - let algo = SizedAlgoKind::from_unsized(algo_kind, algo_byte_len)?; + let algo = SizedAlgoKind::from_unsized(algo_kind, algo_len) + .map_err(|_| LineCheckError::ImproperlyFormatted)?; compute_and_check_digest_from_file(filename_to_check, &expected_checksum, algo, opts) } @@ -779,7 +781,9 @@ fn process_non_algo_based_line( // Remove the leading asterisk if present - only for the first line filename_to_check = &filename_to_check[1..]; } - let expected_checksum = get_raw_expected_digest(&line_info.checksum, None) + + let expected_digest_sum = cli_algo_kind.expected_digest_bit_len(); + let expected_checksum = get_raw_expected_digest(&line_info.checksum, expected_digest_sum) .ok_or(LineCheckError::ImproperlyFormatted)?; // When a specific algorithm name is input, use it and use the provided @@ -789,7 +793,11 @@ fn process_non_algo_based_line( ak::Blake2b | ak::Blake3 => Some(expected_checksum.len()), ak::Sha2 | ak::Sha3 => { // multiplication by 8 to get the number of bits - Some(expected_checksum.len() * 8) + Some( + ShaLength::try_from(expected_checksum.len() * 8) + .map_err(|_| LineCheckError::ImproperlyFormatted)? + .as_usize(), + ) } _ => cli_algo_length, }; diff --git a/src/uucore/src/lib/features/sum.rs b/src/uucore/src/lib/features/sum.rs index 5c998cd0764..7a9f1a5ce58 100644 --- a/src/uucore/src/lib/features/sum.rs +++ b/src/uucore/src/lib/features/sum.rs @@ -176,6 +176,10 @@ impl Digest for Blake3 { #[derive(Default)] pub struct Sm3(sm3::Sm3); +impl Sm3 { + pub const BIT_SIZE: usize = 256; +} + impl Digest for Sm3 { fn hash_update(&mut self, input: &[u8]) { ::update(&mut self.0, input); @@ -190,7 +194,7 @@ impl Digest for Sm3 { } fn output_bits(&self) -> usize { - 256 + Self::BIT_SIZE } } @@ -367,6 +371,9 @@ impl Digest for SysV { // Implements the Digest trait for sha2 / sha3 algorithms with fixed output macro_rules! impl_digest_common { ($algo_type: ty, $size: literal) => { + impl $algo_type { + pub const BIT_SIZE: usize = $size; + } impl Default for $algo_type { fn default() -> Self { Self(Default::default()) @@ -386,7 +393,7 @@ macro_rules! impl_digest_common { } fn output_bits(&self) -> usize { - $size + Self::BIT_SIZE } } }; From a8b47205a93f59443da37b0e7076aa57604e968d Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Sat, 28 Mar 2026 05:25:13 +0900 Subject: [PATCH 080/264] wc: add bench for small file (#11526) --- src/uu/wc/benches/wc_bench.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uu/wc/benches/wc_bench.rs b/src/uu/wc/benches/wc_bench.rs index 67523f0bb67..79f8fbd2fea 100644 --- a/src/uu/wc/benches/wc_bench.rs +++ b/src/uu/wc/benches/wc_bench.rs @@ -8,7 +8,7 @@ use uu_wc::uumain; use uucore::benchmark::{create_test_file, run_util_function, text_data}; /// Benchmark different file sizes for byte counting -#[divan::bench(args = [500])] +#[divan::bench(args = [1, 500])] //todo: add 10kb to measure splice() overhead fn wc_bytes_synthetic(bencher: Bencher, size_mb: usize) { let temp_dir = tempfile::tempdir().unwrap(); let data = text_data::generate_by_size(size_mb, 80); From 6478d6975be2b958f214ac8bca9c7807d63f29a2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 27 Mar 2026 20:26:56 +0000 Subject: [PATCH 081/264] chore(deps): update rust crate clap_mangen to 0.3 --- Cargo.lock | 18 +++++++++--------- Cargo.toml | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index de54b50704f..da2c21cb41d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -385,9 +385,9 @@ checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" [[package]] name = "clap_mangen" -version = "0.2.33" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e30ffc187e2e3aeafcd1c6e2aa416e29739454c0ccaa419226d5ecd181f2d78" +checksum = "d82842b45bf9f6a3be090dd860095ac30728042c08e0d6261ca7259b5d850f07" dependencies = [ "clap", "roff", @@ -992,7 +992,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1709,7 +1709,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde_core", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2012,7 +2012,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2605,7 +2605,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2905,7 +2905,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2915,7 +2915,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4693,7 +4693,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 6a816387454..f0163d40314 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -374,7 +374,7 @@ bytecount = "0.6.8" byteorder = "1.5.0" clap = { version = "4.5", features = ["wrap_help", "cargo", "color"] } clap_complete = "4.4" -clap_mangen = "0.2" +clap_mangen = "0.3" compare = "0.1.0" crossterm = { version = "0.29.0", default-features = false } ctor = "0.6.0" From 248bd1f4efa32561a7c48e7d3d960a062f8785e9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 13 Mar 2026 19:00:12 +0000 Subject: [PATCH 082/264] chore(deps): update rust crate uutils_term_grid to 0.8 --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index da2c21cb41d..a0dcfd87de4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4520,9 +4520,9 @@ dependencies = [ [[package]] name = "uutils_term_grid" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcba141ce511bad08e80b43f02976571072e1ff4286f7d628943efbd277c6361" +checksum = "382d49b39de4a115f203305057741126b09a615892d773a2d419a2b816e30e39" dependencies = [ "ansi-width", ] diff --git a/Cargo.toml b/Cargo.toml index f0163d40314..b749c18652b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -442,7 +442,7 @@ time = { version = "0.3.36" } unicode-width = "0.2.0" unit-prefix = "0.5" utmp-classic = "0.1.6" -uutils_term_grid = "0.7" +uutils_term_grid = "0.8" walkdir = "2.5" winapi-util = "0.1.8" windows-sys = { version = "0.61.0", default-features = false } From 11e299731d4acdc41803a54d5931c9c29f3a2b41 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 28 Mar 2026 13:45:59 +0000 Subject: [PATCH 083/264] chore(deps): update rust crate rustc-hash to v2.1.2 --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a0dcfd87de4..752a9bc9142 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2582,9 +2582,9 @@ dependencies = [ [[package]] name = "rustc-hash" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" [[package]] name = "rustc_version" From 974070ee06dc4879b5e42774eeac5c8ab5c8c0db Mon Sep 17 00:00:00 2001 From: xtqqczze <45661989+xtqqczze@users.noreply.github.com> Date: Fri, 27 Mar 2026 16:13:27 +0000 Subject: [PATCH 084/264] nohup: use LazyLock for FAILURE_CODE --- src/uu/nohup/src/nohup.rs | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/uu/nohup/src/nohup.rs b/src/uu/nohup/src/nohup.rs index c09872c8829..441d3e967d8 100644 --- a/src/uu/nohup/src/nohup.rs +++ b/src/uu/nohup/src/nohup.rs @@ -14,6 +14,7 @@ use std::os::unix::prelude::*; use std::os::unix::process::CommandExt; use std::path::{Path, PathBuf}; use std::process; +use std::sync::LazyLock; use thiserror::Error; use uucore::display::Quotable; use uucore::error::{UError, UResult, set_exit_code}; @@ -55,20 +56,20 @@ impl UError for NohupError { } } -fn failure_code() -> i32 { - if env::var("POSIXLY_CORRECT").is_ok() { +static FAILURE_CODE: LazyLock = LazyLock::new(|| { + if env::var_os("POSIXLY_CORRECT").is_some() { POSIX_NOHUP_FAILURE } else { EXIT_CANCELED } -} +}); #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uucore::clap_localization::handle_clap_result_with_exit_code( uu_app(), args, - failure_code(), + *FAILURE_CODE, )?; replace_fds()?; @@ -136,8 +137,6 @@ fn replace_fds() -> UResult<()> { } fn find_stdout() -> UResult { - let internal_failure_code = failure_code(); - match OpenOptions::new() .create(true) .append(true) @@ -152,7 +151,7 @@ fn find_stdout() -> UResult { } Err(e1) => { let Ok(home) = env::var("HOME") else { - return Err(NohupError::OpenFailed(internal_failure_code, e1).into()); + return Err(NohupError::OpenFailed(*FAILURE_CODE, e1).into()); }; let mut homeout = PathBuf::from(home); homeout.push(NOHUP_OUT); @@ -165,13 +164,12 @@ fn find_stdout() -> UResult { ); Ok(t) } - Err(e2) => Err(NohupError::OpenFailed2( - internal_failure_code, - e1, - homeout_str.to_string(), - e2, - ) - .into()), + Err(e2) => { + Err( + NohupError::OpenFailed2(*FAILURE_CODE, e1, homeout_str.to_string(), e2) + .into(), + ) + } } } } From f5b59c74bb1076abdbf4f27a99533863aaf347ba Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 28 Mar 2026 04:25:07 +0000 Subject: [PATCH 085/264] chore(deps): update rust crate ctor to 0.8.0 --- Cargo.lock | 8 ++++---- Cargo.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 752a9bc9142..d2853a7b404 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -806,9 +806,9 @@ dependencies = [ [[package]] name = "ctor" -version = "0.6.3" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "424e0138278faeb2b401f174ad17e715c829512d74f3d1e81eb43365c2e0590e" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" dependencies = [ "ctor-proc-macro", "dtor", @@ -948,9 +948,9 @@ dependencies = [ [[package]] name = "dtor" -version = "0.1.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "404d02eeb088a82cfd873006cb713fe411306c7d182c344905e101fb1167d301" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" dependencies = [ "dtor-proc-macro", ] diff --git a/Cargo.toml b/Cargo.toml index b749c18652b..e91155af5d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -377,7 +377,7 @@ clap_complete = "4.4" clap_mangen = "0.3" compare = "0.1.0" crossterm = { version = "0.29.0", default-features = false } -ctor = "0.6.0" +ctor = "0.8.0" ctrlc = { version = "3.5.2", features = ["termination"] } divan = { package = "codspeed-divan-compat", version = "4.4.1" } dns-lookup = { version = "3.0.0" } From 23e08f2c7c3742beb224010b91285a205946efb2 Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Sat, 28 Mar 2026 16:43:41 +0100 Subject: [PATCH 086/264] Bump sm3 from 0.4.2 to 0.5.0 --- Cargo.lock | 66 ++++++++++++++++++++++++++++++++++++++++--------- Cargo.toml | 2 +- fuzz/Cargo.lock | 66 ++++++++++++++++++++++++++++++++++++++++--------- 3 files changed, 111 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d2853a7b404..1338b01fe78 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -234,6 +234,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +dependencies = [ + "hybrid-array", +] + [[package]] name = "block2" version = "0.6.2" @@ -493,6 +502,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "const-random" version = "0.1.18" @@ -722,7 +737,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fd92aca2c6001b1bf5ba0ff84ee74ec8501b52bbef0cac80bf25a6c1d87a83d" dependencies = [ "crc", - "digest", + "digest 0.10.7", "rustversion", "spin", ] @@ -804,6 +819,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" +dependencies = [ + "hybrid-array", +] + [[package]] name = "ctor" version = "0.8.0" @@ -878,8 +902,19 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "crypto-common", + "block-buffer 0.10.4", + "crypto-common 0.1.7", +] + +[[package]] +name = "digest" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" +dependencies = [ + "block-buffer 0.12.0", + "const-oid", + "crypto-common 0.2.1", ] [[package]] @@ -1312,6 +1347,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "hybrid-array" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8655f91cd07f2b9d0c24137bd650fe69617773435ee5ec83022377777ce65ef1" +dependencies = [ + "typenum", +] + [[package]] name = "iana-time-zone" version = "0.1.65" @@ -1892,7 +1936,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ "cfg-if", - "digest", + "digest 0.10.7", ] [[package]] @@ -2719,7 +2763,7 @@ checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures 0.2.17", - "digest", + "digest 0.10.7", ] [[package]] @@ -2730,7 +2774,7 @@ checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures 0.2.17", - "digest", + "digest 0.10.7", ] [[package]] @@ -2739,7 +2783,7 @@ version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" dependencies = [ - "digest", + "digest 0.10.7", "keccak", ] @@ -2800,11 +2844,11 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "sm3" -version = "0.4.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebb9a3b702d0a7e33bc4d85a14456633d2b165c2ad839c5fd9a8417c1ab15860" +checksum = "da6a89ba31723d185fd7413b98c576a575f356d9b84729d8ecb6ead60000a5b6" dependencies = [ - "digest", + "digest 0.11.2", ] [[package]] @@ -4439,7 +4483,7 @@ dependencies = [ "crc-fast", "data-encoding", "data-encoding-macro", - "digest", + "digest 0.10.7", "dns-lookup", "dunce", "fluent", diff --git a/Cargo.toml b/Cargo.toml index e91155af5d6..ca30e573a2b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -456,7 +456,7 @@ sha2 = "0.10.8" sha3 = "0.10.8" blake2b_simd = "1.0.2" blake3 = "1.5.1" -sm3 = "0.4.2" +sm3 = "0.5.0" crc-fast = { version = "1.5.0", default-features = false } digest = "0.10.7" diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index 9363e1f46ce..ac43508bf3a 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -178,6 +178,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +dependencies = [ + "hybrid-array", +] + [[package]] name = "block2" version = "0.6.2" @@ -318,6 +327,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "const-random" version = "0.1.18" @@ -383,7 +398,7 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e75b2483e97a5a7da73ac68a05b629f9c53cff58d8ed1c77866079e18b00dba5" dependencies = [ - "digest", + "digest 0.10.7", "spin", ] @@ -437,6 +452,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" +dependencies = [ + "hybrid-array", +] + [[package]] name = "ctrlc" version = "3.5.2" @@ -480,8 +504,19 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "crypto-common", + "block-buffer 0.10.4", + "crypto-common 0.1.7", +] + +[[package]] +name = "digest" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" +dependencies = [ + "block-buffer 0.12.0", + "const-oid", + "crypto-common 0.2.1", ] [[package]] @@ -726,6 +761,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hybrid-array" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8655f91cd07f2b9d0c24137bd650fe69617773435ee5ec83022377777ce65ef1" +dependencies = [ + "typenum", +] + [[package]] name = "iana-time-zone" version = "0.1.65" @@ -1203,7 +1247,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ "cfg-if", - "digest", + "digest 0.10.7", ] [[package]] @@ -1648,7 +1692,7 @@ checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures 0.2.17", - "digest", + "digest 0.10.7", ] [[package]] @@ -1659,7 +1703,7 @@ checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures 0.2.17", - "digest", + "digest 0.10.7", ] [[package]] @@ -1668,7 +1712,7 @@ version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" dependencies = [ - "digest", + "digest 0.10.7", "keccak", ] @@ -1692,11 +1736,11 @@ checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" [[package]] name = "sm3" -version = "0.4.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebb9a3b702d0a7e33bc4d85a14456633d2b165c2ad839c5fd9a8417c1ab15860" +checksum = "da6a89ba31723d185fd7413b98c576a575f356d9b84729d8ecb6ead60000a5b6" dependencies = [ - "digest", + "digest 0.11.2", ] [[package]] @@ -2076,7 +2120,7 @@ dependencies = [ "crc-fast", "data-encoding", "data-encoding-macro", - "digest", + "digest 0.10.7", "dunce", "fluent", "fluent-bundle", From 6c9646a6041e8d78489ed8268fff323eb64c988b Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Sat, 28 Mar 2026 16:50:39 +0100 Subject: [PATCH 087/264] deny.toml: add crates to skip list digest, crypto-common, block-buffer --- deny.toml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/deny.toml b/deny.toml index 2db6d69d05e..ea8396df467 100644 --- a/deny.toml +++ b/deny.toml @@ -108,6 +108,12 @@ skip = [ { name = "foldhash", version = "0.1.5" }, # keccak, sha1, sha2 { name = "cpufeatures", version = "0.2.17" }, + # various crates + { name = "digest", version = "0.10.7" }, + # digest + { name = "crypto-common", version = "0.1.7" }, + # digest + { name = "block-buffer", version = "0.10.4" }, ] # spell-checker: enable From 4e8b99b202dffb1684334d4cb811c5e8010d225e Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 27 Mar 2026 11:10:32 +0100 Subject: [PATCH 088/264] ls,pr: wrap angle-bracket placeholders in backticks to fix mdbook HTML warnings mdbook interprets tokens like , , etc. as unclosed HTML tags. Wrapping them in backticks renders them as code instead. --- src/uu/ls/locales/en-US.ftl | 4 ++-- src/uu/ls/locales/fr-FR.ftl | 4 ++-- src/uu/pr/locales/en-US.ftl | 16 ++++++++-------- src/uu/pr/locales/fr-FR.ftl | 16 ++++++++-------- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/uu/ls/locales/en-US.ftl b/src/uu/ls/locales/en-US.ftl index 7de4a17ce2e..01556554347 100644 --- a/src/uu/ls/locales/en-US.ftl +++ b/src/uu/ls/locales/en-US.ftl @@ -55,7 +55,7 @@ ls-help-escape-quoting-style = Use escape quoting style. Equivalent to `--quotin ls-help-c-quoting-style = Use C quoting style. Equivalent to `--quoting-style=c` ls-help-replace-control-chars = Replace control characters with '?' if they are not escaped. ls-help-show-control-chars = Show control characters 'as is' if they are not escaped. -ls-help-show-time-field = Show time in : +ls-help-show-time-field = Show time in ``: access time (-u): atime, access, use; change time (-t): ctime, status. modification time: mtime, modification. @@ -71,7 +71,7 @@ ls-help-time-access = If the long listing format (e.g., -l, -o) is being used, p ls-help-hide-pattern = do not list implied entries matching shell PATTERN (overridden by -a or -A) ls-help-ignore-pattern = do not list implied entries matching shell PATTERN ls-help-ignore-backups = Ignore entries which end with ~. -ls-help-sort-by-field = Sort by : name, none (-U), time (-t), size (-S), extension (-X) or width +ls-help-sort-by-field = Sort by ``: name, none (-U), time (-t), size (-S), extension (-X) or width ls-help-sort-by-size = Sort by file size, largest first. ls-help-sort-by-time = Sort by modification time (the 'mtime' in the inode), newest first. ls-help-sort-by-version = Natural sort of (version) numbers in the filenames. diff --git a/src/uu/ls/locales/fr-FR.ftl b/src/uu/ls/locales/fr-FR.ftl index bf2b97f9f22..bd1da934b90 100644 --- a/src/uu/ls/locales/fr-FR.ftl +++ b/src/uu/ls/locales/fr-FR.ftl @@ -55,7 +55,7 @@ ls-help-escape-quoting-style = Utiliser le style de citation d'échappement. Éq ls-help-c-quoting-style = Utiliser le style de citation C. Équivalent à `--quoting-style=c` ls-help-replace-control-chars = Remplacer les caractères de contrôle par '?' s'ils ne sont pas échappés. ls-help-show-control-chars = Afficher les caractères de contrôle 'tels quels' s'ils ne sont pas échappés. -ls-help-show-time-field = Afficher l'heure dans : +ls-help-show-time-field = Afficher l'heure dans `` : heure d'accès (-u) : atime, access, use ; heure de changement (-t) : ctime, status. heure de modification : mtime, modification. @@ -71,7 +71,7 @@ ls-help-time-access = Si le format de liste long (par ex., -l, -o) est utilisé, ls-help-hide-pattern = ne pas lister les entrées implicites correspondant au MOTIF shell (surchargé par -a ou -A) ls-help-ignore-pattern = ne pas lister les entrées implicites correspondant au MOTIF shell ls-help-ignore-backups = Ignorer les entrées qui se terminent par ~. -ls-help-sort-by-field = Trier par : name, none (-U), time (-t), size (-S), extension (-X) ou width +ls-help-sort-by-field = Trier par `` : name, none (-U), time (-t), size (-S), extension (-X) ou width ls-help-sort-by-size = Trier par taille de fichier, le plus grand en premier. ls-help-sort-by-time = Trier par heure de modification (le 'mtime' dans l'inode), le plus récent en premier. ls-help-sort-by-version = Tri naturel des numéros (de version) dans les noms de fichiers. diff --git a/src/uu/pr/locales/en-US.ftl b/src/uu/pr/locales/en-US.ftl index 59af328dcb9..946dea2cb51 100644 --- a/src/uu/pr/locales/en-US.ftl +++ b/src/uu/pr/locales/en-US.ftl @@ -16,14 +16,14 @@ pr-help-header = pr-help-date-format = Use 'date'-style FORMAT for the header date. pr-help-double-space = - Produce output that is double spaced. An extra - character is output following every found in the input. + Produce output that is double spaced. An extra `` + character is output following every `` found in the input. pr-help-number-lines = Provide width digit line numbering. The default for width, if not specified, is 5. The number occupies the first width column positions of each text column or each line of -m output. If char (any non-digit character) is given, it is appended to the line number - to separate it from whatever follows. The default for char is a . + to separate it from whatever follows. The default for char is a ``. Line numbers longer than width columns are truncated. pr-help-first-line-number = start counting with NUMBER at 1st line of first page printed pr-help-omit-header = @@ -41,8 +41,8 @@ pr-help-page-length = option were in effect. pr-help-no-file-warnings = omit warning when a file cannot be opened pr-help-form-feed = - Use a for new pages, instead of the default behavior that - uses a sequence of s. + Use a `` for new pages, instead of the default behavior that + uses a sequence of ``s. pr-help-column-width = Set the width of the line to width column positions for multiple text-column output only. If the -w option is not specified and the -s option @@ -67,10 +67,10 @@ pr-help-column = When used with -t, use the minimum number of lines to write the output. pr-help-column-char-separator = Separate text columns by the single character char instead of by the - appropriate number of s (default for char is the character). + appropriate number of ``s (default for char is the `` character). pr-help-column-string-separator = separate columns by STRING, - without -S: Default separator with -J and + without -S: Default separator `` with -J and `` otherwise (same as -S\" \"), no effect on column options pr-help-merge = Merge files. Standard output shall be formatted so the pr utility @@ -78,7 +78,7 @@ pr-help-merge = into text columns of equal fixed widths, in terms of the number of column positions. Implementations shall support merging of at least nine file operands. pr-help-indent = - Each line of output shall be preceded by offset s. If the -o + Each line of output shall be preceded by offset ``s. If the -o option is not specified, the default offset shall be zero. The space taken is in addition to the output line width (see the -w option below). pr-help-join-lines = diff --git a/src/uu/pr/locales/fr-FR.ftl b/src/uu/pr/locales/fr-FR.ftl index 9d9a23407f8..28c6ab8f0bc 100644 --- a/src/uu/pr/locales/fr-FR.ftl +++ b/src/uu/pr/locales/fr-FR.ftl @@ -16,15 +16,15 @@ pr-help-header = pr-help-date-format = Utiliser le FORMAT de style 'date' pour la date dans la ligne d'en-tête. pr-help-double-space = - Produire une sortie avec double espacement. Un caractère - supplémentaire est affiché après chaque trouvé dans l'entrée. + Produire une sortie avec double espacement. Un caractère `` + supplémentaire est affiché après chaque `` trouvé dans l'entrée. pr-help-number-lines = Fournir une numérotation de ligne avec largeur de chiffres. La valeur par défaut pour la largeur, si non spécifiée, est 5. Le numéro occupe les premières largeur positions de colonne de chaque colonne de texte ou de chaque ligne de sortie -m. Si char (tout caractère non numérique) est donné, il est ajouté au numéro de ligne pour le séparer de ce qui suit. La valeur par - défaut pour char est une . Les numéros de ligne plus longs + défaut pour char est une ``. Les numéros de ligne plus longs que largeur colonnes sont tronqués. pr-help-first-line-number = commencer le comptage avec NUMÉRO à la 1ère ligne de la première page imprimée pr-help-omit-header = @@ -39,8 +39,8 @@ pr-help-page-length = était en vigueur. pr-help-no-file-warnings = omettre l'avertissement lorsqu'un fichier ne peut pas être ouvert pr-help-form-feed = - Utiliser un pour les nouvelles pages, au lieu du comportement par défaut - qui utilise une séquence de . + Utiliser un `` pour les nouvelles pages, au lieu du comportement par défaut + qui utilise une séquence de ``. pr-help-column-width = Définir la largeur de la ligne à largeur positions de colonne pour la sortie multi-colonnes de texte seulement. Si l'option -w n'est pas spécifiée et @@ -66,10 +66,10 @@ pr-help-column = utiliser le nombre minimum de lignes pour écrire la sortie. pr-help-column-char-separator = Séparer les colonnes de texte par le caractère unique char au lieu du nombre - approprié d' (par défaut pour char est le caractère de ). + approprié d'`` (par défaut pour char est le caractère de ``). pr-help-column-string-separator = séparer les colonnes par CHAÎNE, - sans -S : Séparateur par défaut avec -J et + sans -S : Séparateur par défaut `` avec -J et `` sinon (même que -S\" \"), aucun effet sur les options de colonne pr-help-merge = Fusionner les fichiers. La sortie standard sera formatée pour que l'utilitaire pr @@ -77,7 +77,7 @@ pr-help-merge = dans des colonnes de texte de largeurs fixes égales, en termes du nombre de positions de colonne. Les implémentations doivent supporter la fusion d'au moins neuf opérandes de fichier. pr-help-indent = - Chaque ligne de sortie sera précédée par décalage . Si l'option -o + Chaque ligne de sortie sera précédée par décalage ``. Si l'option -o n'est pas spécifiée, le décalage par défaut sera zéro. L'espace pris est en plus de la largeur de ligne de sortie (voir l'option -w ci-dessous). pr-help-join-lines = From 53758e183d5d837a05bb27f3d9ae397ecc9f86b7 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 29 Mar 2026 19:26:27 +0200 Subject: [PATCH 089/264] pre-commit: add Cargo.lock sync check Catch out-of-date Cargo.lock and fuzz/Cargo.lock before push, avoiding CI failures from lock file mismatch. --- .pre-commit-config.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 845b03f1a80..bd857250175 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -43,6 +43,13 @@ repos: pass_filenames: false types: [file, rust] language: system + - id: cargo-lock-check + name: Cargo.lock sync check + description: Ensure Cargo.lock and fuzz/Cargo.lock are up-to-date. + entry: bash -c 'for dir in . fuzz; do ( cd "$dir" && cargo fetch --locked --quiet ) || { echo "ERROR - $dir/Cargo.lock is out of date. Run -> cd $dir && cargo update"; exit 1; } done' + pass_filenames: false + files: 'Cargo\.(toml|lock)$' + language: system - id: cspell name: Code spell checker (cspell) description: Run cspell to check for spelling errors (if available). From 98a0689a58cf408102521fd9403d10d0ec9f5344 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Mon, 30 Mar 2026 16:02:15 +0900 Subject: [PATCH 090/264] cat: add bench (#11525) * cat: add bench * rename benchmark Co-authored-by: xtqqczze <45661989+xtqqczze@users.noreply.github.com> --------- Co-authored-by: xtqqczze <45661989+xtqqczze@users.noreply.github.com> --- .github/workflows/benchmarks.yml | 1 + Cargo.lock | 1 + src/uu/cat/Cargo.toml | 6 ++++++ src/uu/cat/benches/cat_bench.rs | 19 +++++++++++++++++++ 4 files changed, 27 insertions(+) create mode 100644 src/uu/cat/benches/cat_bench.rs diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 8466b564962..d2d13275266 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -30,6 +30,7 @@ jobs: uu_base64, uu_cksum, uu_cp, + uu_cat, uu_cut, uu_dd, uu_df, diff --git a/Cargo.lock b/Cargo.lock index 1338b01fe78..50974c9168c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3285,6 +3285,7 @@ name = "uu_cat" version = "0.8.0" dependencies = [ "clap", + "codspeed-divan-compat", "fluent", "memchr", "nix", diff --git a/src/uu/cat/Cargo.toml b/src/uu/cat/Cargo.toml index 880972fa6d4..753efdbde52 100644 --- a/src/uu/cat/Cargo.toml +++ b/src/uu/cat/Cargo.toml @@ -33,8 +33,14 @@ winapi-util = { workspace = true } windows-sys = { workspace = true, features = ["Win32_Storage_FileSystem"] } [dev-dependencies] +divan = { workspace = true } tempfile = { workspace = true } +uucore = { workspace = true, features = ["benchmark"] } [[bin]] name = "cat" path = "src/main.rs" + +[[bench]] +name = "cat_bench" +harness = false diff --git a/src/uu/cat/benches/cat_bench.rs b/src/uu/cat/benches/cat_bench.rs new file mode 100644 index 00000000000..b589eb55ed8 --- /dev/null +++ b/src/uu/cat/benches/cat_bench.rs @@ -0,0 +1,19 @@ +use divan::{Bencher, black_box}; +use uu_cat::uumain; +use uucore::benchmark::{run_util_function, setup_test_file}; + +#[divan::bench(args = [10_000])] +fn cat_default(bencher: Bencher, size_bytes: usize) { + let data = vec![b'a'; size_bytes]; + + let file_path = setup_test_file(&data); + let path_str = file_path.to_str().unwrap(); + + bencher.bench(|| { + black_box(run_util_function(uumain, &[path_str])); + }); +} + +fn main() { + divan::main(); +} From 78ffff81dfba8cd7e97b9f0bbea9911299359c82 Mon Sep 17 00:00:00 2001 From: Jeff Bailey Date: Mon, 30 Mar 2026 08:04:59 +0100 Subject: [PATCH 091/264] uucore(fs): make display_permissions_unix cross-platform (#11538) Remove the #[cfg(unix)] gate from display_permissions_unix and its helper get_file_display. Change the parameter from libc::mode_t to u32 (Metadata::mode() already returns u32, so no cast or information loss). Add pub mod mode to uucore::fs with portable u32 constants for all POSIX S_I* values (S_IFMT, S_IFSOCK, S_IFLNK, S_IFREG, S_IFBLK, S_IFDIR, S_IFCHR, S_IFIFO, S_ISUID, S_ISGID, S_ISVTX, and all nine rwx bits). Callers can import uucore::fs::mode::* instead of libc, without requiring a Unix target. Replace the has!() macro calls in display_permissions_unix with explicit bitwise & != 0 checks, which work without a type-specific macro. Update chmod and mv to drop the libc::mode_t import and the as mode_t casts at each call site. Update is_stdin_directory to use mode::S_IFMT and mode::S_IFDIR instead of the libc imports. Remove #[cfg(unix)] from test_display_permissions and test_get_file_display so both tests compile and run on all platforms. --- src/uu/chmod/src/chmod.rs | 13 ++-- src/uu/cp/src/cp.rs | 2 +- src/uu/mv/src/mv.rs | 3 +- src/uucore/src/lib/features/fs.rs | 106 +++++++++++++++++++----------- 4 files changed, 77 insertions(+), 47 deletions(-) diff --git a/src/uu/chmod/src/chmod.rs b/src/uu/chmod/src/chmod.rs index d77378da453..bb46f513984 100644 --- a/src/uu/chmod/src/chmod.rs +++ b/src/uu/chmod/src/chmod.rs @@ -14,7 +14,6 @@ use thiserror::Error; use uucore::display::Quotable; use uucore::error::{ExitCode, UError, UResult, USimpleError, UUsageError, set_exit_code}; use uucore::fs::display_permissions_unix; -use uucore::libc::mode_t; use uucore::mode; use uucore::perms::{TraverseSymlinks, configure_symlink_and_recursion}; @@ -313,8 +312,8 @@ impl Chmoder { /// Report permission changes based on verbose and changes flags fn report_permission_change(&self, file_path: &Path, old_mode: u32, new_mode: u32) { if self.verbose || self.changes { - let current_permissions = display_permissions_unix(old_mode as mode_t, false); - let new_permissions = display_permissions_unix(new_mode as mode_t, false); + let current_permissions = display_permissions_unix(old_mode, false); + let new_permissions = display_permissions_unix(new_mode, false); if new_mode != old_mode { println!( @@ -668,8 +667,8 @@ impl Chmoder { if (new_mode & !naively_expected_new_mode) != 0 { return Err(ChmodError::NewPermissions( file.into(), - display_permissions_unix(new_mode as mode_t, false), - display_permissions_unix(naively_expected_new_mode as mode_t, false), + display_permissions_unix(new_mode, false), + display_permissions_unix(naively_expected_new_mode, false), ) .into()); } @@ -691,8 +690,8 @@ impl Chmoder { println!( "failed to change mode of file {} from {fperm:04o} ({}) to {mode:04o} ({})", file.quote(), - display_permissions_unix(fperm as mode_t, false), - display_permissions_unix(mode as mode_t, false) + display_permissions_unix(fperm, false), + display_permissions_unix(mode, false) ); } Err(1) diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index 403a8892da5..e781cc14ba4 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -1620,7 +1620,7 @@ fn file_mode_for_interactive_overwrite( Some(( format!("{mode_without_leading_digits:04o}"), - uucore::fs::display_permissions_unix(mode, false), + uucore::fs::display_permissions_unix(mode as u32, false), )) } } diff --git a/src/uu/mv/src/mv.rs b/src/uu/mv/src/mv.rs index 517e70a4fc6..2f69801e15a 100644 --- a/src/uu/mv/src/mv.rs +++ b/src/uu/mv/src/mv.rs @@ -1255,14 +1255,13 @@ fn is_writable(path: &Path) -> (bool, Option) { #[cfg(unix)] fn get_interactive_prompt(to: &Path, cached_mode: Option) -> String { - use libc::mode_t; // Use cached mode if available, otherwise fetch it let mode = cached_mode.or_else(|| to.metadata().ok().map(|m| m.permissions().mode())); if let Some(mode) = mode { let file_mode = mode & 0o777; // Check if file is not writable by user if (mode & 0o200) == 0 { - let perms = display_permissions_unix(mode as mode_t, false); + let perms = display_permissions_unix(mode, false); let mode_info = format!("{file_mode:04o} ({perms})"); return translate!("mv-prompt-overwrite-mode", "target" => to.quote(), "mode_info" => mode_info); } diff --git a/src/uucore/src/lib/features/fs.rs b/src/uucore/src/lib/features/fs.rs index 5e10f2ea513..6ef229d175c 100644 --- a/src/uucore/src/lib/features/fs.rs +++ b/src/uucore/src/lib/features/fs.rs @@ -8,11 +8,7 @@ // spell-checker:ignore backport #[cfg(unix)] -use libc::{ - S_IFBLK, S_IFCHR, S_IFDIR, S_IFIFO, S_IFLNK, S_IFMT, S_IFREG, S_IFSOCK, S_IRGRP, S_IROTH, - S_IRUSR, S_ISGID, S_ISUID, S_ISVTX, S_IWGRP, S_IWOTH, S_IWUSR, S_IXGRP, S_IXOTH, S_IXUSR, - mkfifo, mode_t, -}; +use libc::mkfifo; #[cfg(all(unix, not(target_os = "redox")))] pub use libc::{major, makedev, minor}; use std::collections::HashSet; @@ -464,12 +460,44 @@ pub fn display_permissions(metadata: &fs::Metadata, display_file_type: bool) -> #[cfg(unix)] /// Display the permissions of a file pub fn display_permissions(metadata: &fs::Metadata, display_file_type: bool) -> String { - let mode: mode_t = metadata.mode() as mode_t; - display_permissions_unix(mode, display_file_type) + display_permissions_unix(metadata.mode(), display_file_type) +} + +/// Portable file mode bit constants, equivalent to the POSIX `S_I*` values. +/// +/// These are defined here as plain `u32` so they are available on every +/// platform, including Windows, without requiring `libc` or a Unix target. +/// Callers that previously used `libc::S_IFDIR` etc. can import these instead. +pub mod mode { + // File-type mask and values + pub const S_IFMT: u32 = 0o170_000; // bitmask for the file-type field + pub const S_IFSOCK: u32 = 0o140_000; // socket + pub const S_IFLNK: u32 = 0o120_000; // symbolic link + pub const S_IFREG: u32 = 0o100_000; // regular file + pub const S_IFBLK: u32 = 0o060_000; // block device + pub const S_IFDIR: u32 = 0o040_000; // directory + pub const S_IFCHR: u32 = 0o020_000; // character device + pub const S_IFIFO: u32 = 0o010_000; // named pipe (FIFO) + + // Permission and special-mode bits + pub const S_ISUID: u32 = 0o4000; // setuid + pub const S_ISGID: u32 = 0o2000; // setgid + pub const S_ISVTX: u32 = 0o1000; // sticky + + pub const S_IRUSR: u32 = 0o0400; // owner read + pub const S_IWUSR: u32 = 0o0200; // owner write + pub const S_IXUSR: u32 = 0o0100; // owner execute + + pub const S_IRGRP: u32 = 0o0040; // group read + pub const S_IWGRP: u32 = 0o0020; // group write + pub const S_IXGRP: u32 = 0o0010; // group execute + + pub const S_IROTH: u32 = 0o0004; // other read + pub const S_IWOTH: u32 = 0o0002; // other write + pub const S_IXOTH: u32 = 0o0001; // other execute } /// Returns a character representation of the file type based on its mode. -/// This function is specific to Unix-like systems. /// /// - `mode`: The mode of the file, typically obtained from file metadata. /// @@ -482,8 +510,8 @@ pub fn display_permissions(metadata: &fs::Metadata, display_file_type: bool) -> /// - 'l' for symbolic links /// - 's' for sockets /// - '?' for any other unrecognized file types -#[cfg(unix)] -fn get_file_display(mode: mode_t) -> char { +fn get_file_display(mode: u32) -> char { + use mode::{S_IFBLK, S_IFCHR, S_IFDIR, S_IFIFO, S_IFLNK, S_IFMT, S_IFREG, S_IFSOCK}; match mode & S_IFMT { S_IFDIR => 'd', S_IFCHR => 'c', @@ -500,9 +528,12 @@ fn get_file_display(mode: mode_t) -> char { // The logic below is more readable written this way. #[allow(clippy::if_not_else)] #[allow(clippy::cognitive_complexity)] -#[cfg(unix)] -/// Display the permissions of a file on a unix like system -pub fn display_permissions_unix(mode: mode_t, display_file_type: bool) -> String { +/// Display the unix permissions of a file +pub fn display_permissions_unix(mode: u32, display_file_type: bool) -> String { + use mode::{ + S_IRGRP, S_IROTH, S_IRUSR, S_ISGID, S_ISUID, S_ISVTX, S_IWGRP, S_IWOTH, S_IWUSR, S_IXGRP, + S_IXOTH, S_IXUSR, + }; let mut result; if display_file_type { result = String::with_capacity(10); @@ -511,31 +542,31 @@ pub fn display_permissions_unix(mode: mode_t, display_file_type: bool) -> String result = String::with_capacity(9); } - result.push(if has!(mode, S_IRUSR) { 'r' } else { '-' }); - result.push(if has!(mode, S_IWUSR) { 'w' } else { '-' }); - result.push(if has!(mode, S_ISUID as mode_t) { - if has!(mode, S_IXUSR) { 's' } else { 'S' } - } else if has!(mode, S_IXUSR) { + result.push(if mode & S_IRUSR != 0 { 'r' } else { '-' }); + result.push(if mode & S_IWUSR != 0 { 'w' } else { '-' }); + result.push(if mode & S_ISUID != 0 { + if mode & S_IXUSR != 0 { 's' } else { 'S' } + } else if mode & S_IXUSR != 0 { 'x' } else { '-' }); - result.push(if has!(mode, S_IRGRP) { 'r' } else { '-' }); - result.push(if has!(mode, S_IWGRP) { 'w' } else { '-' }); - result.push(if has!(mode, S_ISGID as mode_t) { - if has!(mode, S_IXGRP) { 's' } else { 'S' } - } else if has!(mode, S_IXGRP) { + result.push(if mode & S_IRGRP != 0 { 'r' } else { '-' }); + result.push(if mode & S_IWGRP != 0 { 'w' } else { '-' }); + result.push(if mode & S_ISGID != 0 { + if mode & S_IXGRP != 0 { 's' } else { 'S' } + } else if mode & S_IXGRP != 0 { 'x' } else { '-' }); - result.push(if has!(mode, S_IROTH) { 'r' } else { '-' }); - result.push(if has!(mode, S_IWOTH) { 'w' } else { '-' }); - result.push(if has!(mode, S_ISVTX as mode_t) { - if has!(mode, S_IXOTH) { 't' } else { 'T' } - } else if has!(mode, S_IXOTH) { + result.push(if mode & S_IROTH != 0 { 'r' } else { '-' }); + result.push(if mode & S_IWOTH != 0 { 'w' } else { '-' }); + result.push(if mode & S_ISVTX != 0 { + if mode & S_IXOTH != 0 { 't' } else { 'T' } + } else if mode & S_IXOTH != 0 { 'x' } else { '-' @@ -730,8 +761,9 @@ pub fn path_ends_with_terminator(path: &Path) -> bool { pub fn is_stdin_directory(stdin: &Stdin) -> bool { #[cfg(unix)] { + use mode::{S_IFDIR, S_IFMT}; use nix::sys::stat::fstat; - let mode = fstat(stdin.as_fd()).unwrap().st_mode as mode_t; + let mode = fstat(stdin.as_fd()).unwrap().st_mode as u32; // We use the S_IFMT mask ala S_ISDIR() to avoid mistaking // sockets for directories. mode & S_IFMT == S_IFDIR @@ -964,9 +996,9 @@ mod tests { } } - #[cfg(unix)] #[test] fn test_display_permissions() { + use mode::*; // spell-checker:ignore (perms) brwsr drwxr rwxr assert_eq!( "drwxr-xr-x", @@ -992,29 +1024,29 @@ mod tests { assert_eq!( "brwSr-xr-x", - display_permissions_unix(S_IFBLK | S_ISUID as mode_t | 0o655, true) + display_permissions_unix(S_IFBLK | S_ISUID | 0o655, true) ); assert_eq!( "brwsr-xr-x", - display_permissions_unix(S_IFBLK | S_ISUID as mode_t | 0o755, true) + display_permissions_unix(S_IFBLK | S_ISUID | 0o755, true) ); assert_eq!( "prw---sr--", - display_permissions_unix(S_IFIFO | S_ISGID as mode_t | 0o614, true) + display_permissions_unix(S_IFIFO | S_ISGID | 0o614, true) ); assert_eq!( "prw---Sr--", - display_permissions_unix(S_IFIFO | S_ISGID as mode_t | 0o604, true) + display_permissions_unix(S_IFIFO | S_ISGID | 0o604, true) ); assert_eq!( "c---r-xr-t", - display_permissions_unix(S_IFCHR | S_ISVTX as mode_t | 0o055, true) + display_permissions_unix(S_IFCHR | S_ISVTX | 0o055, true) ); assert_eq!( "c---r-xr-T", - display_permissions_unix(S_IFCHR | S_ISVTX as mode_t | 0o054, true) + display_permissions_unix(S_IFCHR | S_ISVTX | 0o054, true) ); } @@ -1095,9 +1127,9 @@ mod tests { assert!(are_hardlinks_to_same_file(path1, &path2)); } - #[cfg(unix)] #[test] fn test_get_file_display() { + use mode::*; assert_eq!(get_file_display(S_IFDIR | 0o755), 'd'); assert_eq!(get_file_display(S_IFCHR | 0o644), 'c'); assert_eq!(get_file_display(S_IFBLK | 0o600), 'b'); From 602b9331c529678294a38a2a7412f5b4d22b85fe Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Fri, 13 Mar 2026 16:49:04 +0900 Subject: [PATCH 092/264] stdbuf: Fix cfg block for Cygwin --- src/uu/stdbuf/src/stdbuf.rs | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/src/uu/stdbuf/src/stdbuf.rs b/src/uu/stdbuf/src/stdbuf.rs index da0ba7eccee..6355fea0932 100644 --- a/src/uu/stdbuf/src/stdbuf.rs +++ b/src/uu/stdbuf/src/stdbuf.rs @@ -33,14 +33,9 @@ mod options { #[cfg(all( not(feature = "feat_external_libstdbuf"), - any( - target_os = "linux", - target_os = "android", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd", - target_os = "dragonfly" - ) + unix, + not(target_vendor = "apple"), + not(target_os = "cygwin") ))] const STDBUF_INJECT: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/libstdbuf.so")); @@ -84,14 +79,7 @@ enum ProgramOptionsError { ValueTooLarge(String), } -#[cfg(any( - target_os = "linux", - target_os = "android", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd", - target_os = "dragonfly" -))] +#[cfg(all(unix, not(target_vendor = "apple"), not(target_os = "cygwin")))] #[expect( clippy::unnecessary_wraps, reason = "fn sig must match on all platforms" @@ -109,6 +97,15 @@ fn preload_strings() -> UResult<(&'static str, &'static str)> { Ok(("DYLD_LIBRARY_PATH", "dylib")) } +#[cfg(target_os = "cygwin")] +#[expect( + clippy::unnecessary_wraps, + reason = "fn sig must match on all platforms" +)] +fn preload_strings() -> UResult<(&'static str, &'static str)> { + Ok(("LD_PRELOAD", "dll")) +} + fn check_option(matches: &ArgMatches, name: &str) -> Result { match matches.get_one::(name) { Some(value) => match value.as_str() { From 467ac8930f4b20a142106ea90c9dc1b94480ea39 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Sun, 15 Mar 2026 15:01:51 +0900 Subject: [PATCH 093/264] ls: Fix error message for ls file/missing-file --- src/uu/ls/locales/en-US.ftl | 1 + src/uu/ls/src/ls.rs | 1 + tests/by-util/test_ls.rs | 14 ++++++++++++++ 3 files changed, 16 insertions(+) diff --git a/src/uu/ls/locales/en-US.ftl b/src/uu/ls/locales/en-US.ftl index 01556554347..fd6fc38fd59 100644 --- a/src/uu/ls/locales/en-US.ftl +++ b/src/uu/ls/locales/en-US.ftl @@ -22,6 +22,7 @@ ls-error-cannot-open-directory-bad-descriptor = cannot open directory {$path}: B ls-error-unknown-io-error = unknown io error: {$path}, '{$error}' ls-error-invalid-block-size = invalid --block-size argument {$size} ls-error-dired-and-zero-incompatible = --dired and --zero are incompatible +ls-error-not-directory = cannot access {$path}: Not a directory ls-error-not-listing-already-listed = {$path}: not listing already-listed directory ls-error-invalid-time-style = invalid --time-style argument {$style} Possible values are: diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index f689f5f6865..063bd7af1ab 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -68,6 +68,7 @@ enum LsError { IOError(#[from] std::io::Error), #[error("{}", match .1.kind() { + ErrorKind::NotADirectory => translate!("ls-error-not-directory", "path" => .0.quote()), ErrorKind::NotFound => translate!("ls-error-cannot-access-no-such-file", "path" => .0.quote()), ErrorKind::PermissionDenied => match .1.raw_os_error().unwrap_or(1) { 1 => translate!("ls-error-cannot-access-operation-not-permitted", "path" => .0.quote()), diff --git a/tests/by-util/test_ls.rs b/tests/by-util/test_ls.rs index 11365392ae1..fbbba1a0a22 100644 --- a/tests/by-util/test_ls.rs +++ b/tests/by-util/test_ls.rs @@ -57,6 +57,20 @@ const COMMA_ARGS: &[&str] = &["-m", "--format=commas", "--for=commas"]; const COLUMN_ARGS: &[&str] = &["-C", "--format=columns", "--for=columns"]; +#[test] +#[cfg(unix)] +fn test_directory_in_file() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.touch("file"); + + scene + .ucmd() + .arg("file/missing") + .fails_with_code(2) + .stderr_is("ls: cannot access 'file/missing': Not a directory\n"); +} + #[test] fn test_invalid_flag() { new_ucmd!() From b621af6872f70308697089e3766ccbfe313567fe Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Mon, 30 Mar 2026 16:56:37 +0900 Subject: [PATCH 094/264] stdbuf: remove clippy::unnecessary_wraps --- src/uu/stdbuf/src/stdbuf.rs | 28 ++++++++-------------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/src/uu/stdbuf/src/stdbuf.rs b/src/uu/stdbuf/src/stdbuf.rs index 6355fea0932..60a7206a65d 100644 --- a/src/uu/stdbuf/src/stdbuf.rs +++ b/src/uu/stdbuf/src/stdbuf.rs @@ -80,30 +80,18 @@ enum ProgramOptionsError { } #[cfg(all(unix, not(target_vendor = "apple"), not(target_os = "cygwin")))] -#[expect( - clippy::unnecessary_wraps, - reason = "fn sig must match on all platforms" -)] -fn preload_strings() -> UResult<(&'static str, &'static str)> { - Ok(("LD_PRELOAD", "so")) +fn preload_strings() -> (&'static str, &'static str) { + ("LD_PRELOAD", "so") } #[cfg(target_vendor = "apple")] -#[expect( - clippy::unnecessary_wraps, - reason = "fn sig must match on all platforms" -)] -fn preload_strings() -> UResult<(&'static str, &'static str)> { - Ok(("DYLD_LIBRARY_PATH", "dylib")) +fn preload_strings() -> (&'static str, &'static str) { + ("DYLD_LIBRARY_PATH", "dylib") } #[cfg(target_os = "cygwin")] -#[expect( - clippy::unnecessary_wraps, - reason = "fn sig must match on all platforms" -)] -fn preload_strings() -> UResult<(&'static str, &'static str)> { - Ok(("LD_PRELOAD", "dll")) +fn preload_strings() -> (&'static str, &'static str) { + ("LD_PRELOAD", "dll") } fn check_option(matches: &ArgMatches, name: &str) -> Result { @@ -146,7 +134,7 @@ fn get_preload_env(tmp_dir: &TempDir) -> UResult<(String, PathBuf)> { use std::fs::File; use std::io::Write; - let (preload, extension) = preload_strings()?; + let (preload, extension) = preload_strings(); let inject_path = tmp_dir.path().join("libstdbuf").with_extension(extension); let mut file = File::create(&inject_path)?; @@ -157,7 +145,7 @@ fn get_preload_env(tmp_dir: &TempDir) -> UResult<(String, PathBuf)> { #[cfg(feature = "feat_external_libstdbuf")] fn get_preload_env(_tmp_dir: &TempDir) -> UResult<(String, PathBuf)> { - let (preload, extension) = preload_strings()?; + let (preload, extension) = preload_strings(); // Use the directory provided at compile time via LIBSTDBUF_DIR environment variable // This will fail to compile if LIBSTDBUF_DIR is not set, which is the desired behavior From befb7b91ba3dc60394799ed9478f1d40f653a77f Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Mon, 23 Mar 2026 06:17:06 +0900 Subject: [PATCH 095/264] coreutils: --list arg and no args should fail --- src/bin/coreutils.rs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/bin/coreutils.rs b/src/bin/coreutils.rs index 383115baa82..f81f40ff67b 100644 --- a/src/bin/coreutils.rs +++ b/src/bin/coreutils.rs @@ -85,10 +85,10 @@ fn main() { match util { "--list" => { - // If --help is also present, show usage instead of list - if args.any(|arg| arg == "--help" || arg == "-h") { - usage(&utils, binary_as_util); - process::exit(0); + // we should fail with additional args https://github.com/uutils/coreutils/issues/11383#issuecomment-4082564058 + if args.next().is_some() { + let _ = writeln!(io::stderr(), "coreutils: invalid argument"); + process::exit(1); } let mut out = io::stdout().lock(); for util in utils.keys() { @@ -156,8 +156,13 @@ fn main() { } } } else { - // no arguments provided - usage(&utils, binary_as_util); - process::exit(0); + // GNU just fails, but busybox tests needs usage + // todo: patch the test suite instead + if binary_as_util.ends_with("box") { + usage(&utils, binary_as_util); + } else { + let _ = writeln!(io::stderr(), "coreutils: missing argument"); + } + process::exit(1); } } From d6102baa7663e218b1c437f9e60188ed33d51949 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Mon, 30 Mar 2026 20:02:41 +0900 Subject: [PATCH 096/264] CONTRIBUTING.md: drop outdated hint, add policies and links (#11433) Co-authored-by: xtqqczze <45661989+xtqqczze@users.noreply.github.com> --- CONTRIBUTING.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a8e463707f0..b614f44396f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -51,6 +51,8 @@ crates is as follows: We have separated repositories for crates that we maintain but also publish for use by others: +- [coreutils-i10n](https://github.com/uutils/coreutils-l10n) +- [num-prime](https://github.com/uutils/num-prime) - [uutils-term-grid](https://github.com/uutils/uutils-term-grid) - [parse_datetime](https://github.com/uutils/parse_datetime) @@ -78,6 +80,7 @@ issues and writing documentation are just as important as writing code. We can't fix bugs we don't know about, so good issues are super helpful! Here are some tips for writing good issues: +- Confirm the bug is in coreutils; some tools (e.g., `find`, `sed`) are maintained in separate repositories under the uutils project. - If you find a bug, make sure it's still a problem on the [`main` branch](https://github.com/uutils/coreutils/releases/tag/latest-commit). - Search through the existing issues to see whether it has already been reported. @@ -123,7 +126,7 @@ submit a patch! ### Don't `panic!` The coreutils should be very reliable. This means that we should never `panic!`. -Therefore, you should avoid using `.unwrap()` and `panic!`. Sometimes the use of +Therefore, you should avoid using `println!`, `.unwrap()` and `panic!`. Sometimes the use of `unreachable!` can be justified with a comment explaining why that code is unreachable. @@ -260,9 +263,6 @@ you contribute must at least compile without warnings for all platforms in the CI. However, you can use `#[cfg(...)]` attributes to create platform dependent features. -**Tip:** For Windows, Microsoft provides some images (VMWare, Hyper-V, -VirtualBox and Parallels) for development [on their official download page](https://developer.microsoft.com/windows/downloads/virtual-machines/). - ## Improving the GNU compatibility Please make sure you have installed From d01e62e05fc7ab5c9e70c7c7c99c8a2d245d81a4 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Mon, 30 Mar 2026 20:40:48 +0900 Subject: [PATCH 097/264] stdbuf: use .with_capacity for Vec storing searched paths --- src/uu/stdbuf/src/stdbuf.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uu/stdbuf/src/stdbuf.rs b/src/uu/stdbuf/src/stdbuf.rs index 60a7206a65d..f8927967d8b 100644 --- a/src/uu/stdbuf/src/stdbuf.rs +++ b/src/uu/stdbuf/src/stdbuf.rs @@ -154,7 +154,7 @@ fn get_preload_env(_tmp_dir: &TempDir) -> UResult<(String, PathBuf)> { // Search paths in order: // 1. Directory where stdbuf is located (program_path) // 2. Compile-time directory from LIBSTDBUF_DIR - let mut search_paths: Vec = Vec::new(); + let mut search_paths: Vec = Vec::with_capacity(2); // First, try to get the directory where stdbuf is running from if let Ok(exe_path) = std::env::current_exe() { From 153e6d653177c7f60466b92a3d3de03c5fc9e611 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Thu, 26 Mar 2026 06:32:05 +0900 Subject: [PATCH 098/264] CICD.yml: Bump cross for NetBSD --- .github/workflows/CICD.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index c07747c1b8e..4ccb92267b7 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -511,13 +511,9 @@ jobs: if: steps.vars.outputs.CARGO_CMD == 'cross' && matrix.job.target != 'x86_64-unknown-netbsd' with: tool: cross@0.2.5 - - uses: dtolnay/rust-toolchain@master - if: steps.vars.outputs.CARGO_CMD == 'cross' && matrix.job.target == 'x86_64-unknown-netbsd' - with: - toolchain: "1.92.0" - name: Install cross from git (NetBSD) if: steps.vars.outputs.CARGO_CMD == 'cross' && matrix.job.target == 'x86_64-unknown-netbsd' - run: cargo +1.92.0 install cross --git https://github.com/cross-rs/cross --rev 588b3c99db52b5a9c5906fab96cfadcf1bde7863 + run: cargo install cross --git https://github.com/cross-rs/cross --rev f86fd03bb70b4c6802847c18087e21391498b0b4 - name: Create all needed build/work directories shell: bash run: | From b482d4c49efa1eddc1b4bff8bcc91fa1b3377553 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Thu, 26 Mar 2026 05:40:30 +0900 Subject: [PATCH 099/264] pathchk: build for Windows --- Cargo.toml | 4 ++-- src/uu/pathchk/src/pathchk.rs | 22 ++++++++++++++++++---- tests/by-util/test_pathchk.rs | 7 +++++++ 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ca30e573a2b..00fef376bcf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -131,6 +131,7 @@ feat_common_core = [ "numfmt", "od", "paste", + "pathchk", "pr", "printenv", "printf", @@ -198,6 +199,7 @@ feat_wasm = [ "numfmt", "od", "paste", + "pathchk", "pr", "printenv", "printf", @@ -287,7 +289,6 @@ feat_require_unix_core = [ "mknod", "nice", "nohup", - "pathchk", "stat", "stty", "timeout", @@ -316,7 +317,6 @@ feat_os_unix_fuchsia = [ "mkfifo", "mknod", "nice", - "pathchk", "tty", "uname", "unlink", diff --git a/src/uu/pathchk/src/pathchk.rs b/src/uu/pathchk/src/pathchk.rs index 8ba3f4d37ac..2bf7b40a802 100644 --- a/src/uu/pathchk/src/pathchk.rs +++ b/src/uu/pathchk/src/pathchk.rs @@ -33,6 +33,20 @@ mod options { const POSIX_PATH_MAX: usize = 256; const POSIX_NAME_MAX: usize = 14; +#[cfg(all(unix, not(target_os = "redox")))] +const PATH_MAX: usize = libc::PATH_MAX as usize; +#[cfg(all(unix, not(target_os = "redox")))] +const FILENAME_MAX: usize = libc::FILENAME_MAX as usize; +#[cfg(target_os = "redox")] +const PATH_MAX: usize = 4096; +#[cfg(target_os = "redox")] +const FILENAME_MAX: usize = 255; +// for Windows. But don't deny wasm +#[cfg(not(unix))] +const PATH_MAX: usize = 260; +#[cfg(not(unix))] +const FILENAME_MAX: usize = 255; + #[uucore::main(no_signals)] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?; @@ -193,11 +207,11 @@ fn check_default(path: &[String]) -> bool { let joined_path = path.join("/"); let total_len = joined_path.len(); // path length - if total_len > libc::PATH_MAX as usize { + if total_len > PATH_MAX { writeln!( std::io::stderr(), "{}", - translate!("pathchk-error-path-length-exceeded", "limit" => libc::PATH_MAX, "length" => total_len, "path" => joined_path.quote()) + translate!("pathchk-error-path-length-exceeded", "limit" => PATH_MAX, "length" => total_len, "path" => joined_path.quote()) ); return false; } @@ -219,11 +233,11 @@ fn check_default(path: &[String]) -> bool { // components: length for p in path { let component_len = p.len(); - if component_len > libc::FILENAME_MAX as usize { + if component_len > FILENAME_MAX { writeln!( std::io::stderr(), "{}", - translate!("pathchk-error-name-length-exceeded", "limit" => libc::FILENAME_MAX, "length" => component_len, "component" => p.quote()) + translate!("pathchk-error-name-length-exceeded", "limit" => FILENAME_MAX, "length" => component_len, "component" => p.quote()) ); return false; } diff --git a/tests/by-util/test_pathchk.rs b/tests/by-util/test_pathchk.rs index 85f5c09ea0b..47fb1e01d1b 100644 --- a/tests/by-util/test_pathchk.rs +++ b/tests/by-util/test_pathchk.rs @@ -4,9 +4,11 @@ // file that was distributed with this source code. #[cfg(target_os = "linux")] use std::os::unix::ffi::OsStringExt; +#[cfg(unix)] use uutests::new_ucmd; #[test] +#[cfg(unix)] fn test_no_args() { new_ucmd!() .fails() @@ -15,11 +17,13 @@ fn test_no_args() { } #[test] +#[cfg(unix)] fn test_invalid_arg() { new_ucmd!().arg("--definitely-invalid").fails_with_code(1); } #[test] +#[cfg(unix)] fn test_default_mode() { // accept some reasonable default new_ucmd!().args(&["dir/file"]).succeeds().no_stdout(); @@ -55,6 +59,7 @@ fn test_default_mode() { } #[test] +#[cfg(unix)] fn test_posix_mode() { // accept some reasonable default new_ucmd!().args(&["-p", "dir/file"]).succeeds().no_stdout(); @@ -79,6 +84,7 @@ fn test_posix_mode() { } #[test] +#[cfg(unix)] fn test_posix_special() { // accept some reasonable default new_ucmd!().args(&["-P", "dir/file"]).succeeds().no_stdout(); @@ -118,6 +124,7 @@ fn test_posix_special() { } #[test] +#[cfg(unix)] fn test_posix_all() { // accept some reasonable default new_ucmd!() From 84b40663264c4406adecb90a2b135b9fea0b257c Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Mon, 30 Mar 2026 16:23:22 +0200 Subject: [PATCH 100/264] sort: fix warnings about unused stuff on Redox (#11547) --- src/uu/sort/src/tmp_dir.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/uu/sort/src/tmp_dir.rs b/src/uu/sort/src/tmp_dir.rs index 2ec48232003..a9b93ceb701 100644 --- a/src/uu/sort/src/tmp_dir.rs +++ b/src/uu/sort/src/tmp_dir.rs @@ -2,18 +2,21 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. + +#[cfg(not(target_os = "redox"))] +use std::path::Path; +#[cfg(not(target_os = "redox"))] use std::sync::atomic::{AtomicBool, Ordering}; use std::{ fs::File, - path::{Path, PathBuf}, + path::PathBuf, sync::{Arc, LazyLock, Mutex}, }; use tempfile::TempDir; -use uucore::{ - error::{UResult, USimpleError}, - show_error, translate, -}; +use uucore::error::UResult; +#[cfg(not(target_os = "redox"))] +use uucore::{error::USimpleError, show_error, translate}; use crate::{SortError, current_open_fd_count, fd_soft_limit}; @@ -181,6 +184,7 @@ impl Drop for TmpDirWrapper { /// Remove the directory at `path` by deleting its child files and then itself. /// Errors while deleting child files are ignored. +#[cfg(not(target_os = "redox"))] fn remove_tmp_dir(path: &Path) -> std::io::Result<()> { if let Ok(read_dir) = std::fs::read_dir(path) { for file in read_dir.flatten() { From 54405fe567b52217fcb8ca94d6aa24ab0f0108dc Mon Sep 17 00:00:00 2001 From: Alvaro Guimaraes <131234+aguimaraes@users.noreply.github.com> Date: Mon, 30 Mar 2026 10:54:50 -0400 Subject: [PATCH 101/264] head: fix error message when input is a directory (#11541) * head: fix error message when input is a directory * head: check for directory before trying to open --- src/uu/head/src/head.rs | 11 +++++++++-- tests/by-util/test_head.rs | 8 ++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/uu/head/src/head.rs b/src/uu/head/src/head.rs index 428d62443a1..bd651bc57aa 100644 --- a/src/uu/head/src/head.rs +++ b/src/uu/head/src/head.rs @@ -13,10 +13,10 @@ use std::io::{self, BufWriter, Read, Seek, SeekFrom, Write}; use std::num::TryFromIntError; #[cfg(unix)] use std::os::fd::AsFd; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use thiserror::Error; use uucore::display::{Quotable, print_verbatim}; -use uucore::error::{FromIo, UError, UResult}; +use uucore::error::{FromIo, UError, UResult, USimpleError}; use uucore::line_ending::LineEnding; use uucore::translate; use uucore::{format_usage, show}; @@ -510,6 +510,13 @@ fn uu_head(options: &HeadOptions) -> UResult<()> { Ok(()) } else { + if Path::new(file).is_dir() { + show!(USimpleError::new( + 1, + translate!("head-error-reading-file", "name" => file.quote(), "err" => "Is a directory") + )); + continue; + } let mut file_handle = match File::open(file) { Ok(f) => f, Err(err) => { diff --git a/tests/by-util/test_head.rs b/tests/by-util/test_head.rs index e35d347f3b3..d954aa9d918 100644 --- a/tests/by-util/test_head.rs +++ b/tests/by-util/test_head.rs @@ -911,3 +911,11 @@ fn test_head_non_utf8_paths() { assert!(output.contains("line3")); } // Test that head handles non-UTF-8 file names without crashing + +#[test] +fn test_do_not_attempt_to_read_a_directory() { + new_ucmd!() + .arg(".") + .fails_with_code(1) + .stderr_contains("error reading '.'"); +} From 2527e2594ba951b1891d3ee358395ad4731366c6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 21:31:02 +0200 Subject: [PATCH 102/264] chore(deps): update rust crate blake3 to v1.8.4 (#11550) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 50974c9168c..075beec0705 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -213,16 +213,16 @@ dependencies = [ [[package]] name = "blake3" -version = "1.8.3" +version = "1.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d" +checksum = "4d2d5991425dfd0785aed03aedcf0b321d61975c9b5b3689c774a2610ae0b51e" dependencies = [ "arrayref", "arrayvec", "cc", "cfg-if", "constant_time_eq", - "cpufeatures 0.2.17", + "cpufeatures 0.3.0", ] [[package]] From cb29f8855f662b157ad580df9ff8b2b73e781a2d Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Tue, 31 Mar 2026 04:31:17 +0900 Subject: [PATCH 103/264] yes: dedup the logic for buffer creation, improve assertion (#11536) --- src/uu/yes/src/yes.rs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/uu/yes/src/yes.rs b/src/uu/yes/src/yes.rs index 732a1bf5877..c9e86f99e38 100644 --- a/src/uu/yes/src/yes.rs +++ b/src/uu/yes/src/yes.rs @@ -92,14 +92,9 @@ fn args_into_buffer<'a>( /// Assumes buf holds a single output line forged from the command line arguments, copies it /// repeatedly until the buffer holds as many copies as it can under [`BUF_SIZE`]. fn prepare_buffer(buf: &mut Vec) { - if buf.len() * 2 > BUF_SIZE { - return; - } - - assert!(!buf.is_empty()); - let line_len = buf.len(); - let target_size = line_len * (BUF_SIZE / line_len); + debug_assert!(line_len > 0, "buffer is not empty since we have newline"); + let target_size = line_len * (BUF_SIZE / line_len); // 0 if line_len is already large enough while buf.len() < target_size { let to_copy = std::cmp::min(target_size - buf.len(), buf.len()); From 8d45386e69de1ea8e0fae03181a336cbb937f0c1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 19:32:10 +0000 Subject: [PATCH 104/264] chore(deps): update rust crate platform-info to v2.1.0 --- Cargo.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 075beec0705..92650066792 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2291,12 +2291,12 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "platform-info" -version = "2.0.5" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7539aeb3fdd8cb4f6a331307cf71a1039cee75e94e8a71725b9484f4a0d9451a" +checksum = "9368d62437c8cbb7c31ee37fd8c08a7d390e09a3ff75698a674953f46705ffcb" dependencies = [ "libc", - "winapi", + "windows-sys 0.59.0", ] [[package]] From 6eb8ede146cc328d3288adb2040d88637cc11af4 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Tue, 31 Mar 2026 15:05:59 +0900 Subject: [PATCH 105/264] nice: Remove unsafe and reduce bin size (#11489) --- .../workspace.wordlist.txt | 1 + Cargo.lock | 3 +-- Cargo.toml | 1 + src/uu/nice/Cargo.toml | 3 +-- src/uu/nice/src/nice.rs | 25 ++++++++----------- 5 files changed, 15 insertions(+), 18 deletions(-) diff --git a/.vscode/cspell.dictionaries/workspace.wordlist.txt b/.vscode/cspell.dictionaries/workspace.wordlist.txt index a5d245016c7..1ae6319c181 100644 --- a/.vscode/cspell.dictionaries/workspace.wordlist.txt +++ b/.vscode/cspell.dictionaries/workspace.wordlist.txt @@ -8,6 +8,7 @@ advapi32-sys aho-corasick backtrace blake2b_simd +rustix # * uutils project uutils diff --git a/Cargo.lock b/Cargo.lock index 92650066792..eb64f01c704 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3828,8 +3828,7 @@ version = "0.8.0" dependencies = [ "clap", "fluent", - "libc", - "nix", + "rustix", "uucore", ] diff --git a/Cargo.toml b/Cargo.toml index 00fef376bcf..a1519260cfa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -430,6 +430,7 @@ rstest = "0.26.0" rstest_reuse = "0.7.0" rustc-hash = "2.1.1" rust-ini = "0.21.0" +rustix = "1.1.4" same-file = "1.0.6" self_cell = "1.0.4" selinux = "=0.6.0" diff --git a/src/uu/nice/Cargo.toml b/src/uu/nice/Cargo.toml index 58dfb69f6af..45703c435d1 100644 --- a/src/uu/nice/Cargo.toml +++ b/src/uu/nice/Cargo.toml @@ -20,12 +20,11 @@ path = "src/nice.rs" [dependencies] clap = { workspace = true } -libc = { workspace = true } uucore = { workspace = true } fluent = { workspace = true } [target.'cfg(unix)'.dependencies] -nix = { workspace = true } +rustix = { workspace = true, features = ["process"] } [[bin]] name = "nice" diff --git a/src/uu/nice/src/nice.rs b/src/uu/nice/src/nice.rs index 1abb7e96ff1..298bef3ba03 100644 --- a/src/uu/nice/src/nice.rs +++ b/src/uu/nice/src/nice.rs @@ -6,9 +6,8 @@ // spell-checker:ignore (ToDO) getpriority setpriority nstr PRIO use clap::{Arg, ArgAction, Command}; -use libc::PRIO_PROCESS; use std::ffi::OsString; -use std::io::{Error, ErrorKind, Write, stdout}; +use std::io::{ErrorKind, Write, stdout}; use std::num::IntErrorKind; use std::os::unix::process::CommandExt; use std::process; @@ -110,14 +109,12 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uucore::clap_localization::handle_clap_result_with_exit_code(uu_app(), args, 125)?; - nix::errno::Errno::clear(); - let mut niceness = unsafe { libc::getpriority(PRIO_PROCESS, 0) }; - if Error::last_os_error().raw_os_error().unwrap() != 0 { - return Err(USimpleError::new( - 125, - format!("getpriority: {}", Error::last_os_error()), - )); - } + let mut niceness = match rustix::process::getpriority_process(None) { + Ok(p) => p, + Err(e) => { + return Err(USimpleError::new(125, format!("getpriority: {e}"))); + } + }; let adjustment = if let Some(nstr) = matches.get_one::(options::ADJUSTMENT) { if !matches.contains_id(options::COMMAND) { @@ -152,8 +149,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // isn't writable. The GNU test suite checks specifically that the // exit code when failing to write the advisory is 125, but Rust // will produce an exit code of 101 when it panics. - if unsafe { libc::setpriority(PRIO_PROCESS, 0, niceness) } == -1 { - let warning_msg = translate!("nice-warning-setpriority", "util_name" => uucore::util_name(), "error" => Error::last_os_error()); + if let Err(e) = rustix::process::setpriority_process(None, niceness) { + let warning_msg = translate!("nice-warning-setpriority", "util_name" => "nice", "error" => e.to_string() ); if write!(std::io::stderr(), "{warning_msg}").is_err() { set_exit_code(125); @@ -179,13 +176,13 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("nice") .about(translate!("nice-about")) .override_usage(format_usage(&translate!("nice-usage"))) .trailing_var_arg(true) .infer_long_args(true) .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("nice")) .arg( Arg::new(options::ADJUSTMENT) .short('n') From e7a667fc1668b6e56332c71838d337992527ea80 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Tue, 31 Mar 2026 15:10:45 +0900 Subject: [PATCH 106/264] arch, uname, Cargo.toml: update outdated info (#11556) --- Cargo.toml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a1519260cfa..4f98541268e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -179,6 +179,7 @@ feat_Tier1 = [ # We don't need to support all of wasm targets. So the ambiguous name is used at here # It is bit complex to deduplicate with other lists feat_wasm = [ + "arch", "basename", "base32", "base64", @@ -213,6 +214,7 @@ feat_wasm = [ "tee", "true", "truncate", + "uname", "unexpand", "uniq", "unlink", @@ -227,9 +229,6 @@ feat_wasm = [ "sha256sum", "sha384sum", "sha512sum", - # useless? - "arch", - "uname", ] # "feat_os_unix" == set of utilities which can be built/run on modern/usual *nix platforms. feat_os_unix = [ From f18c96b362b24932900022b3f47664340e2e225f Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Tue, 31 Mar 2026 08:57:38 +0200 Subject: [PATCH 107/264] pre-commit: exclude tests/fixtures from all hooks --- .pre-commit-config.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bd857250175..2fa0fc01edb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,6 @@ # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks +exclude: ^tests/fixtures/ repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 From 662d9f326899f04f3e143a160e8f5be0574af7d7 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Tue, 31 Mar 2026 09:07:27 +0200 Subject: [PATCH 108/264] pre-commit: fix trailing whitespace and skip cargo hooks in CI --- .github/workflows/CICD.yml | 2 +- src/uu/date/locales/fr-FR.ftl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index 4ccb92267b7..a3e1a194c01 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -631,7 +631,7 @@ jobs: ${{ steps.vars.outputs.CARGO_CMD }} ${{ steps.vars.outputs.CARGO_CMD_OPTIONS }} build --release --config=profile.release.strip=true \ --target=${{ matrix.job.target }} ${{ matrix.job.cargo-options }} ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} ${{ steps.vars.outputs.CARGO_DEFAULT_FEATURES_OPTION }} # We don't want to have many duplicated long jobs at here - # So we build individual binaries for few platforms until we deduplicate many release build for Linux + # So we build individual binaries for few platforms until we deduplicate many release build for Linux - name: Build individual binaries if: matrix.job.skip-tests != true && matrix.job.target == 'x86_64-pc-windows-msvc' shell: bash diff --git a/src/uu/date/locales/fr-FR.ftl b/src/uu/date/locales/fr-FR.ftl index cf827246634..3c09d4164d1 100644 --- a/src/uu/date/locales/fr-FR.ftl +++ b/src/uu/date/locales/fr-FR.ftl @@ -102,5 +102,5 @@ date-error-cannot-set-date = impossible de définir la date date-error-extra-operand = opérande supplémentaire '{$operand}' date-error-write = erreur d'écriture: {$error} date-error-format-missing-plus = l'argument {$arg} ne commence pas par un signe '+'; - lorsqu'une option est utilisée pour spécifier une ou plusieurs dates, tout argument autre + lorsqu'une option est utilisée pour spécifier une ou plusieurs dates, tout argument autre qu'une option doit être une chaîne de format commençant par un signe '+'. From f98ec64e319bd84a931575d89b734f7eab16ddc0 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Tue, 31 Mar 2026 09:13:24 +0200 Subject: [PATCH 109/264] pre-commit: skip cargo and cspell hooks on pre-commit.ci --- .pre-commit-config.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2fa0fc01edb..9abeb5ca476 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -57,3 +57,6 @@ repos: entry: bash -c 'if command -v cspell >/dev/null 2>&1; then cspell --no-must-find-files -- "$@"; else echo "cspell not found, skipping spell check"; exit 0; fi' -- pass_filenames: true language: system + +ci: + skip: [rust-linting, rust-clippy, cargo-lock-check, cspell] From d3ef3f953eb89ca17b2e73a47d3a6991cdc52586 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ey=C3=BCp=20Can=20Akman?= Date: Wed, 25 Mar 2026 21:23:12 +0300 Subject: [PATCH 110/264] chown: warn when '.' is used as owner:group separator When the spec contains '.' but no ':', emit a warning matching GNU coreutils behavior and re-parse with '.' as separator so both owner and group are applied. Closes #11352 --- src/uu/chown/locales/en-US.ftl | 3 ++ src/uu/chown/locales/fr-FR.ftl | 3 ++ src/uu/chown/src/chown.rs | 23 +++++++++---- tests/by-util/test_chown.rs | 63 +++++++++++++++++++++++++++++++++- 4 files changed, 84 insertions(+), 8 deletions(-) diff --git a/src/uu/chown/locales/en-US.ftl b/src/uu/chown/locales/en-US.ftl index 0dfe8301e9d..9bcb725a6c3 100644 --- a/src/uu/chown/locales/en-US.ftl +++ b/src/uu/chown/locales/en-US.ftl @@ -21,3 +21,6 @@ chown-error-failed-to-get-attributes = failed to get attributes of { $file } chown-error-invalid-user = invalid user: { $user } chown-error-invalid-group = invalid group: { $group } chown-error-invalid-spec = invalid spec: { $spec } + +# Warning messages +chown-warning-dot-separator = '.' should be ':': { $spec } diff --git a/src/uu/chown/locales/fr-FR.ftl b/src/uu/chown/locales/fr-FR.ftl index 48e39853a3e..deaa7a620e9 100644 --- a/src/uu/chown/locales/fr-FR.ftl +++ b/src/uu/chown/locales/fr-FR.ftl @@ -21,3 +21,6 @@ chown-error-failed-to-get-attributes = échec de l'obtention des attributs de { chown-error-invalid-user = utilisateur invalide : { $user } chown-error-invalid-group = groupe invalide : { $group } chown-error-invalid-spec = spécification invalide : { $spec } + +# Messages d'avertissement +chown-warning-dot-separator = '.' devrait être ':' : { $spec } diff --git a/src/uu/chown/src/chown.rs b/src/uu/chown/src/chown.rs index 6f0599ce811..23372742817 100644 --- a/src/uu/chown/src/chown.rs +++ b/src/uu/chown/src/chown.rs @@ -9,6 +9,7 @@ use uucore::display::Quotable; pub use uucore::entries::{self, Group, Locate, Passwd}; use uucore::format_usage; use uucore::perms::{GidUidOwnerFilter, IfFrom, chown_base, options}; +use uucore::show_warning; use uucore::translate; use uucore::error::{FromIo, UResult, USimpleError}; @@ -151,7 +152,7 @@ pub fn uu_app() -> Command { } /// Parses the user string to extract the UID. -fn parse_uid(user: &str, spec: &str, sep: char) -> UResult> { +fn parse_uid(user: &str, spec: &str) -> UResult> { if user.is_empty() { return Ok(None); } @@ -160,11 +161,6 @@ fn parse_uid(user: &str, spec: &str, sep: char) -> UResult> { return Ok(Some(u.uid)); } - // Handle `username.groupname` syntax (e.g. when sep is ':' but spec contains '.') - if spec.contains('.') && !spec.contains(':') && sep == ':' { - return parse_spec(spec, '.').map(|(uid, _)| uid); - } - // Fallback: `user` string contains a numeric user ID user.parse().map(Some).map_err(|_| { USimpleError::new( @@ -209,7 +205,20 @@ fn parse_spec(spec: &str, sep: char) -> UResult<(Option, Option)> { let user = args.next().unwrap_or(""); let group = args.next().unwrap_or(""); - let uid = parse_uid(user, spec, sep)?; + // dot separator: try as username first, fall back to owner.group (like GNU) + if sep == ':' && !spec.contains(':') && spec.contains('.') { + if let Ok(uid) = parse_uid(user, spec) { + let gid = parse_gid(group, spec)?; + return Ok((uid, gid)); + } + show_warning!( + "{}", + translate!("chown-warning-dot-separator", "spec" => spec.quote()) + ); + return parse_spec(spec, '.'); + } + + let uid = parse_uid(user, spec)?; let gid = parse_gid(group, spec)?; if user.chars().next().is_some_and(char::is_numeric) && group.is_empty() && spec != user { diff --git a/tests/by-util/test_chown.rs b/tests/by-util/test_chown.rs index 14b7f84d859..f7c08b4a054 100644 --- a/tests/by-util/test_chown.rs +++ b/tests/by-util/test_chown.rs @@ -149,7 +149,8 @@ fn test_chown_only_owner_colon() { .arg("--verbose") .arg(file1) .succeeds() - .stderr_contains("retained as"); + .stderr_contains("retained as") + .stderr_contains("warning: '.' should be ':'"); scene .ucmd() @@ -160,6 +161,66 @@ fn test_chown_only_owner_colon() { .stderr_contains("failed to change"); } +#[test] +fn test_chown_dot_separator_warning() { + // test that using '.' as separator emits a warning + + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let result = scene.cmd("whoami").run(); + if skipping_test_is_okay(&result, "whoami: cannot find name for user ID") { + return; + } + let user_name = String::from(result.stdout_str().trim()); + assert!(!user_name.is_empty()); + + let file1 = "test_chown_dot_warn"; + at.touch(file1); + + let result = scene.cmd("id").arg("-gn").run(); + if skipping_test_is_okay(&result, "id: cannot find name for group ID") { + return; + } + let group_name = String::from(result.stdout_str().trim()); + assert!(!group_name.is_empty()); + + // chown user. file should warn about '.' separator + scene + .ucmd() + .arg(format!("{user_name}.")) + .arg(file1) + .succeeds() + .stderr_contains("warning: '.' should be ':'"); + + // chown user.group file should warn AND apply both owner and group + let result = scene + .ucmd() + .arg(format!("{user_name}.{group_name}")) + .arg("--verbose") + .arg(file1) + .run(); + if skipping_test_is_okay(&result, "chown: invalid group:") { + return; + } + result.stderr_contains("warning: '.' should be ':'"); + // "retained as" on Linux, "changed ownership" on BSDs (group inherited from parent dir) + assert!( + result.stderr_str().contains("retained as") + || result.stderr_str().contains("changed ownership"), + "expected verbose ownership output, got: {}", + result.stderr_str() + ); + + // chown user: file should not warn + scene + .ucmd() + .arg(format!("{user_name}:")) + .arg(file1) + .succeeds() + .stderr_does_not_contain("warning"); +} + #[test] fn test_chown_only_colon() { // test chown : file.txt From 7dcf762f8d5856c23a01617ccda0cc44b10b4e35 Mon Sep 17 00:00:00 2001 From: Kevin Burke Date: Thu, 19 Mar 2026 21:26:38 -0700 Subject: [PATCH 111/264] uucore: support realtime signals (RTMIN/RTMAX) in signal_by_name_or_value `signal_by_name_or_value()` only looked up signals from the static ALL_SIGNALS array, which doesn't include realtime signals. This caused `timeout -s RTMAX`, `kill -s RTMIN`, and `env --default-signal=RTMAX` to reject valid signal names. Extend the function to fall back to realtime_signal_bounds() for both name lookups and numeric values in the realtime range, matching the behavior already present in signal_list_value_by_name_or_number(). Also fix timeout's report_if_verbose() to use signal_list_name_by_value instead of signal_name_by_value, so verbose output doesn't panic for realtime signal numbers. Fixes GNU test: tests/env/env-signal-handler.sh Co-Authored-By: Claude Opus 4.6 --- .../workspace.wordlist.txt | 4 +++ src/uu/timeout/src/timeout.rs | 4 +-- src/uucore/src/lib/features/signals.rs | 33 +++++++++++++++++-- tests/by-util/test_timeout.rs | 18 ++++++++++ 4 files changed, 55 insertions(+), 4 deletions(-) diff --git a/.vscode/cspell.dictionaries/workspace.wordlist.txt b/.vscode/cspell.dictionaries/workspace.wordlist.txt index 1ae6319c181..72f2d95f11e 100644 --- a/.vscode/cspell.dictionaries/workspace.wordlist.txt +++ b/.vscode/cspell.dictionaries/workspace.wordlist.txt @@ -185,9 +185,13 @@ LINESIZE NAMESIZE RTLD_NEXT RTLD +RTMAX +RTMIN SIGABRT SIGINT SIGKILL +SIGRTMAX +SIGRTMIN SIGSTOP SIGTERM SYS_fdatasync diff --git a/src/uu/timeout/src/timeout.rs b/src/uu/timeout/src/timeout.rs index 6944794f19c..cb483db8fb8 100644 --- a/src/uu/timeout/src/timeout.rs +++ b/src/uu/timeout/src/timeout.rs @@ -22,7 +22,7 @@ use uucore::translate; use uucore::{ format_usage, - signals::{signal_by_name_or_value, signal_name_by_value}, + signals::{signal_by_name_or_value, signal_list_name_by_value}, }; use nix::sys::signal::{SigHandler, Signal, kill}; @@ -227,7 +227,7 @@ fn report_if_verbose(signal: usize, cmd: &str, verbose: bool) { let s = if signal == 0 { "0".to_string() } else { - signal_name_by_value(signal).unwrap().to_string() + signal_list_name_by_value(signal).unwrap() }; let mut stderr = std::io::stderr(); let _ = writeln!( diff --git a/src/uucore/src/lib/features/signals.rs b/src/uucore/src/lib/features/signals.rs index 6b89704fcad..a5141f299e3 100644 --- a/src/uucore/src/lib/features/signals.rs +++ b/src/uucore/src/lib/features/signals.rs @@ -396,11 +396,24 @@ pub static ALL_SIGNALS: [&str; 32] = [ pub fn signal_by_name_or_value(signal_name_or_value: &str) -> Option { let signal_name_upcase = signal_name_or_value.to_uppercase(); if let Ok(value) = signal_name_upcase.parse() { - return if is_signal(value) { Some(value) } else { None }; + if is_signal(value) { + return Some(value); + } + return realtime_signal_bounds() + .filter(|&(rtmin, rtmax)| value >= rtmin && value <= rtmax) + .map(|_| value); } let signal_name = signal_name_upcase.trim_start_matches("SIG"); - ALL_SIGNALS.iter().position(|&s| s == signal_name) + if let Some(pos) = ALL_SIGNALS.iter().position(|&s| s == signal_name) { + return Some(pos); + } + + realtime_signal_bounds().and_then(|(rtmin, rtmax)| match signal_name { + "RTMIN" => Some(rtmin), + "RTMAX" => Some(rtmax), + _ => None, + }) } /// Returns true if the given number is a valid signal number. @@ -754,3 +767,19 @@ fn linux_unnamed_signal_numbers_are_valid_for_lists() { assert_eq!(signal_list_value_by_name_or_number("32"), Some(32)); assert_eq!(signal_list_value_by_name_or_number("33"), Some(33)); } + +#[cfg(any(target_os = "linux", target_os = "android"))] +#[test] +fn linux_realtime_signals_resolve_by_name_or_value() { + let (rtmin, rtmax) = realtime_signal_bounds().unwrap(); + + // By name + assert_eq!(signal_by_name_or_value("RTMIN"), Some(rtmin)); + assert_eq!(signal_by_name_or_value("RTMAX"), Some(rtmax)); + assert_eq!(signal_by_name_or_value("SIGRTMIN"), Some(rtmin)); + assert_eq!(signal_by_name_or_value("SIGRTMAX"), Some(rtmax)); + + // By numeric value + assert_eq!(signal_by_name_or_value(&rtmin.to_string()), Some(rtmin)); + assert_eq!(signal_by_name_or_value(&rtmax.to_string()), Some(rtmax)); +} diff --git a/tests/by-util/test_timeout.rs b/tests/by-util/test_timeout.rs index 4bbc532e9cc..bb2ea78b825 100644 --- a/tests/by-util/test_timeout.rs +++ b/tests/by-util/test_timeout.rs @@ -286,3 +286,21 @@ fn test_foreground_signal0_kill_after() { .args(&["--foreground", "-s0", "-k.1", ".1", "sleep", "10"]) .fails_with_code(137); } + +#[test] +#[cfg(any(target_os = "linux", target_os = "android"))] +fn test_realtime_signal_names() { + // timeout should accept RTMIN and RTMAX as valid signal names + new_ucmd!() + .args(&["-v", "-s", "RTMAX", ".1", "sleep", "1"]) + .fails() + .stderr_contains("sending signal RTMAX to command"); + new_ucmd!() + .args(&["-v", "-s", "RTMIN", ".1", "sleep", "1"]) + .fails() + .stderr_contains("sending signal RTMIN to command"); + new_ucmd!() + .args(&["-v", "-s", "SIGRTMAX", ".1", "sleep", "1"]) + .fails() + .stderr_contains("sending signal RTMAX to command"); +} From 151689a0b196de9192aa28b7fd496567f88f6942 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=A2=E7=92=83=E9=85=B1?= Date: Tue, 31 Mar 2026 21:03:22 +0800 Subject: [PATCH 112/264] wc: fix for 32-bit Android (#11452) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 白彩恋 <169267914+shirorren@users.noreply.github.com> --- src/uu/wc/src/wc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uu/wc/src/wc.rs b/src/uu/wc/src/wc.rs index 0c09c2c3a43..eaabbce8c28 100644 --- a/src/uu/wc/src/wc.rs +++ b/src/uu/wc/src/wc.rs @@ -302,7 +302,7 @@ fn is_stdin_small_file() -> bool { matches!( stat::fstat(io::stdin().as_fd()), - Ok(meta) if meta.st_mode & libc::S_IFMT == libc::S_IFREG && meta.st_size <= (10 << 20) + Ok(meta) if meta.st_mode as libc::mode_t & libc::S_IFMT == libc::S_IFREG && meta.st_size <= (10 << 20) ) } From fa1d77787debe4c7f1d5c0bc410204a979196a3c Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Mon, 30 Mar 2026 18:37:23 +0900 Subject: [PATCH 113/264] coreutils: strip errno --- src/bin/coreutils.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/bin/coreutils.rs b/src/bin/coreutils.rs index f81f40ff67b..84c7ff0ab85 100644 --- a/src/bin/coreutils.rs +++ b/src/bin/coreutils.rs @@ -10,7 +10,7 @@ use std::cmp; use std::ffi::OsString; use std::io::{self, Write}; use std::process; -use uucore::Args; +use uucore::{Args, error::strip_errno}; const VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -44,7 +44,7 @@ Currently defined functions: if let Err(e) = writeln!(io::stdout(), "{s}") && e.kind() != io::ErrorKind::BrokenPipe { - let _ = writeln!(io::stderr(), "coreutils: {e}"); + let _ = writeln!(io::stderr(), "coreutils: {}", strip_errno(&e)); process::exit(1); } } @@ -95,7 +95,7 @@ fn main() { if let Err(e) = writeln!(out, "{util}") && e.kind() != io::ErrorKind::BrokenPipe { - let _ = writeln!(io::stderr(), "coreutils: {e}"); + let _ = writeln!(io::stderr(), "coreutils: {}", strip_errno(&e)); process::exit(1); } } @@ -105,7 +105,7 @@ fn main() { if let Err(e) = writeln!(io::stdout(), "coreutils {VERSION} (multi-call binary)") && e.kind() != io::ErrorKind::BrokenPipe { - let _ = writeln!(io::stderr(), "coreutils: {e}"); + let _ = writeln!(io::stderr(), "coreutils: {}", strip_errno(&e)); process::exit(1); } process::exit(0); From a8734f0b48bf71b6bd05b30e2ce6eaf201946c51 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Wed, 1 Apr 2026 00:56:15 +0900 Subject: [PATCH 114/264] uucore: use rustix'splice & restrict to Linux --- Cargo.lock | 1 + fuzz/Cargo.lock | 1 + src/uucore/Cargo.toml | 1 + src/uucore/src/lib/features/pipes.rs | 32 +++++++++++++++++----------- 4 files changed, 22 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index eb64f01c704..5dd54a97d2a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4508,6 +4508,7 @@ dependencies = [ "os_display", "procfs", "rustc-hash", + "rustix", "selinux", "sha1", "sha2", diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index ac43508bf3a..bd3186e0b18 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -2144,6 +2144,7 @@ dependencies = [ "os_display", "procfs", "rustc-hash", + "rustix", "sha1", "sha2", "sha3", diff --git a/src/uucore/Cargo.toml b/src/uucore/Cargo.toml index 2f11ecfd30a..3f1c9af89ef 100644 --- a/src/uucore/Cargo.toml +++ b/src/uucore/Cargo.toml @@ -37,6 +37,7 @@ jiff = { workspace = true, optional = true, features = [ "tzdb-concatenated", ] } rustc-hash = { workspace = true } +rustix = { workspace = true, features = ["pipe"] } time = { workspace = true, optional = true, features = [ "formatting", "local-offset", diff --git a/src/uucore/src/lib/features/pipes.rs b/src/uucore/src/lib/features/pipes.rs index 069736c5215..776cf2711aa 100644 --- a/src/uucore/src/lib/features/pipes.rs +++ b/src/uucore/src/lib/features/pipes.rs @@ -3,27 +3,26 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -//! Thin pipe-related wrappers around functions from the `nix` crate. - +//! Thin zero-copy-related wrappers around functions from the `rustix` crate. +#[cfg(any(target_os = "linux", target_os = "android"))] use std::fs::File; #[cfg(any(target_os = "linux", target_os = "android"))] use std::os::fd::AsFd; #[cfg(any(target_os = "linux", target_os = "android"))] -use nix::fcntl::SpliceFFlags; +use rustix::pipe::SpliceFlags; -pub use nix::{Error, Result}; - -/// A wrapper around [`nix::unistd::pipe`] that ensures the pipe is cleaned up. +/// A wrapper around [`rustix::pipe::pipe`] that ensures the pipe is cleaned up. /// /// Returns two `File` objects: everything written to the second can be read /// from the first. -pub fn pipe() -> Result<(File, File)> { - let (read, write) = nix::unistd::pipe()?; +#[cfg(any(target_os = "linux", target_os = "android"))] +pub fn pipe() -> std::io::Result<(File, File)> { + let (read, write) = rustix::pipe::pipe()?; Ok((File::from(read), File::from(write))) } -/// Less noisy wrapper around [`nix::fcntl::splice`]. +/// Less noisy wrapper around [`rustix::pipe::splice`]. /// /// Up to `len` bytes are moved from `source` to `target`. Returns the number /// of successfully moved bytes. @@ -33,8 +32,15 @@ pub fn pipe() -> Result<(File, File)> { /// a [`pipe`] and then from the pipe into your target (with `splice_exact`): /// this is still very efficient. #[cfg(any(target_os = "linux", target_os = "android"))] -pub fn splice(source: &impl AsFd, target: &impl AsFd, len: usize) -> Result { - nix::fcntl::splice(source, None, target, None, len, SpliceFFlags::empty()) +pub fn splice(source: &impl AsFd, target: &impl AsFd, len: usize) -> std::io::Result { + Ok(rustix::pipe::splice( + source, + None, + target, + None, + len, + SpliceFlags::empty(), + )?) } /// Splice wrapper which fully finishes the write. @@ -43,11 +49,11 @@ pub fn splice(source: &impl AsFd, target: &impl AsFd, len: usize) -> Result Result<()> { +pub fn splice_exact(source: &impl AsFd, target: &impl AsFd, len: usize) -> std::io::Result<()> { let mut left = len; while left != 0 { let written = splice(source, target, left)?; - assert_ne!(written, 0, "unexpected end of data"); + debug_assert_ne!(written, 0, "unexpected end of data"); left -= written; } Ok(()) From 0451024ce9b86df2b570396fb7aa9e3b830412e7 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Wed, 1 Apr 2026 02:51:15 +0900 Subject: [PATCH 115/264] cat: replace nix with rustix --- Cargo.lock | 2 +- src/uu/cat/Cargo.toml | 2 +- src/uu/cat/src/cat.rs | 4 ++-- src/uu/cat/src/platform/unix.rs | 14 ++++++-------- src/uu/cat/src/splice.rs | 18 +++++++++--------- 5 files changed, 19 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5dd54a97d2a..adf882f2232 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3288,7 +3288,7 @@ dependencies = [ "codspeed-divan-compat", "fluent", "memchr", - "nix", + "rustix", "tempfile", "thiserror 2.0.18", "uucore", diff --git a/src/uu/cat/Cargo.toml b/src/uu/cat/Cargo.toml index 753efdbde52..e5c8ca35b86 100644 --- a/src/uu/cat/Cargo.toml +++ b/src/uu/cat/Cargo.toml @@ -26,7 +26,7 @@ uucore = { workspace = true, features = ["fast-inc", "fs", "pipes", "signals"] } fluent = { workspace = true } [target.'cfg(unix)'.dependencies] -nix = { workspace = true } +rustix = { workspace = true, features = ["fs"] } [target.'cfg(windows)'.dependencies] winapi-util = { workspace = true } diff --git a/src/uu/cat/src/cat.rs b/src/uu/cat/src/cat.rs index aef7d5eab50..46e4e1be911 100644 --- a/src/uu/cat/src/cat.rs +++ b/src/uu/cat/src/cat.rs @@ -84,10 +84,10 @@ enum CatError { /// Wrapper around `io::Error` #[error("{}", strip_errno(.0))] Io(#[from] io::Error), - /// Wrapper around `nix::Error` + /// Wrapper around `rustix::io::Errno` #[cfg(any(target_os = "linux", target_os = "android"))] #[error("{0}")] - Nix(#[from] nix::Error), + Rustix(#[from] rustix::io::Errno), /// Unknown file type; it's not a regular file, socket, etc. #[error("{}", translate!("cat-error-unknown-filetype", "ft_debug" => .ft_debug))] UnknownFiletype { diff --git a/src/uu/cat/src/platform/unix.rs b/src/uu/cat/src/platform/unix.rs index b2cdda2aa67..6cc55fc6967 100644 --- a/src/uu/cat/src/platform/unix.rs +++ b/src/uu/cat/src/platform/unix.rs @@ -5,8 +5,7 @@ // spell-checker:ignore lseek seekable -use nix::fcntl::{FcntlArg, OFlag, fcntl}; -use nix::unistd::{Whence, lseek}; +use rustix::fs::{OFlags, SeekFrom, fcntl_getfl}; use std::os::fd::AsFd; use uucore::fs::FileInformation; @@ -31,10 +30,10 @@ pub fn is_unsafe_overwrite(input: &I, output: &O) -> bool { if file_size == 0 { return false; } - // `lseek` returns an error if the file descriptor is closed or it refers to + // `seek` returns an error if the file descriptor is closed or it refers to // a non-seekable resource (e.g., pipe, socket, or some devices). - let input_pos = lseek(input.as_fd(), 0, Whence::SeekCur); - let output_pos = lseek(output.as_fd(), 0, Whence::SeekCur); + let input_pos = rustix::fs::seek(input, SeekFrom::Current(0)).map(|v| v as i64); + let output_pos = rustix::fs::seek(output, SeekFrom::Current(0)).map(|v| v as i64); if is_appending(output) { if let Ok(pos) = input_pos { if pos >= 0 && (pos as u64) >= file_size { @@ -54,9 +53,8 @@ pub fn is_unsafe_overwrite(input: &I, output: &O) -> bool { /// Whether the file is opened with the `O_APPEND` flag fn is_appending(file: &F) -> bool { - let flags_raw = fcntl(file.as_fd(), FcntlArg::F_GETFL).unwrap_or_default(); - let flags = OFlag::from_bits_truncate(flags_raw); - flags.contains(OFlag::O_APPEND) + let flags = fcntl_getfl(file).unwrap_or(OFlags::empty()); + flags.contains(OFlags::APPEND) } #[cfg(test)] diff --git a/src/uu/cat/src/splice.rs b/src/uu/cat/src/splice.rs index ca5265d2bf8..a8b01e22605 100644 --- a/src/uu/cat/src/splice.rs +++ b/src/uu/cat/src/splice.rs @@ -4,7 +4,7 @@ // file that was distributed with this source code. use super::{CatResult, FdReadable, InputHandle}; -use nix::unistd; +use rustix::io::{read, write}; use std::os::{fd::AsFd, unix::io::AsRawFd}; use uucore::pipes::{pipe, splice, splice_exact}; @@ -52,20 +52,20 @@ pub(super) fn write_fast_using_splice( /// Move exactly `num_bytes` bytes from `read_fd` to `write_fd`. /// /// Panics if not enough bytes can be read. -fn copy_exact(read_fd: &impl AsFd, write_fd: &impl AsFd, num_bytes: usize) -> nix::Result<()> { +fn copy_exact(read_fd: &impl AsFd, write_fd: &impl AsFd, num_bytes: usize) -> std::io::Result<()> { let mut left = num_bytes; let mut buf = [0; BUF_SIZE]; while left > 0 { - let read = unistd::read(read_fd, &mut buf)?; - assert_ne!(read, 0, "unexpected end of pipe"); + let n = read(read_fd, &mut buf)?; + assert_ne!(n, 0, "unexpected end of pipe"); let mut written = 0; - while written < read { - match unistd::write(write_fd, &buf[written..read])? { - 0 => panic!(), - n => written += n, + while written < n { + match write(write_fd, &buf[written..n])? { + 0 => unreachable!("fd should be writable"), + w => written += w, } } - left -= read; + left -= n; } Ok(()) } From 7ff01db6c0c15cf3c98115adbacc3f1fd2d4e1b2 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Tue, 31 Mar 2026 23:57:32 +0900 Subject: [PATCH 116/264] dd: catch OOM --- src/uu/dd/src/dd.rs | 10 ++++++---- tests/by-util/test_dd.rs | 8 ++++++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/uu/dd/src/dd.rs b/src/uu/dd/src/dd.rs index e1d7b0ad6b2..6c7ce253d42 100644 --- a/src/uu/dd/src/dd.rs +++ b/src/uu/dd/src/dd.rs @@ -1166,10 +1166,6 @@ fn dd_copy(mut i: Input, o: Output) -> io::Result<()> { ); } - // Create a common buffer with a capacity of the block size. - // This is the max size needed. - let mut buf = vec![BUF_INIT_BYTE; bsize]; - // Spawn a timer thread to provide a scheduled signal indicating when we // should send an update of our progress to the reporting thread. // @@ -1201,6 +1197,11 @@ fn dd_copy(mut i: Input, o: Output) -> io::Result<()> { BlockWriter::Unbuffered(o) }; + // Create a common empty buffer with a capacity of the block size. + // This is the max size needed. + let mut buf = Vec::new(); + buf.try_reserve(bsize)?; // try_with_capacity is unstable https://github.com/rust-lang/rust/issues/91913 + // The main read/write loop. // // Each iteration reads blocks from the input and writes @@ -1366,6 +1367,7 @@ fn read_helper(i: &mut Input, buf: &mut Vec, bsize: usize) -> io::Result Date: Wed, 1 Apr 2026 13:20:31 +0900 Subject: [PATCH 117/264] df: replace nix by rustix --- Cargo.lock | 2 +- src/uu/df/Cargo.toml | 2 +- src/uu/df/src/df.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index adf882f2232..ae22c7d5040 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3461,7 +3461,7 @@ dependencies = [ "clap", "codspeed-divan-compat", "fluent", - "nix", + "rustix", "tempfile", "thiserror 2.0.18", "unicode-width 0.2.2", diff --git a/src/uu/df/Cargo.toml b/src/uu/df/Cargo.toml index 9b99baa9287..dfb5aea8def 100644 --- a/src/uu/df/Cargo.toml +++ b/src/uu/df/Cargo.toml @@ -26,7 +26,7 @@ thiserror = { workspace = true } fluent = { workspace = true } [target.'cfg(unix)'.dependencies] -nix = { workspace = true, features = ["fs"] } +rustix = { workspace = true, features = ["fs"] } [dev-dependencies] divan = { workspace = true } diff --git a/src/uu/df/src/df.rs b/src/uu/df/src/df.rs index 51b950dd460..2f97814d02b 100644 --- a/src/uu/df/src/df.rs +++ b/src/uu/df/src/df.rs @@ -301,7 +301,7 @@ fn get_all_filesystems(opt: &Options) -> UResult> { // Run a sync call before any operation if so instructed. if opt.sync { #[cfg(not(any(windows, target_os = "redox")))] - nix::unistd::sync(); + rustix::fs::sync(); } let mut mounts = vec![]; From 7a78c99cf618194e22197992745be7ea49e2fa10 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Wed, 1 Apr 2026 13:00:47 +0900 Subject: [PATCH 118/264] wc: replace nix by rustix --- Cargo.lock | 2 +- fuzz/Cargo.lock | 2 +- src/uu/wc/Cargo.toml | 2 +- src/uu/wc/src/count_fast.rs | 8 ++++---- src/uu/wc/src/wc.rs | 3 +-- 5 files changed, 8 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ae22c7d5040..b340ac549ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4433,7 +4433,7 @@ dependencies = [ "codspeed-divan-compat", "fluent", "libc", - "nix", + "rustix", "tempfile", "thiserror 2.0.18", "unicode-width 0.2.2", diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index bd3186e0b18..da97b02dc8d 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -2102,7 +2102,7 @@ dependencies = [ "clap", "fluent", "libc", - "nix", + "rustix", "thiserror", "unicode-width", "uucore", diff --git a/src/uu/wc/Cargo.toml b/src/uu/wc/Cargo.toml index b4c2005af2b..a285d58c8a9 100644 --- a/src/uu/wc/Cargo.toml +++ b/src/uu/wc/Cargo.toml @@ -33,7 +33,7 @@ unicode-width = { workspace = true } [target.'cfg(unix)'.dependencies] libc = { workspace = true } -nix = { workspace = true } +rustix = { workspace = true, features = ["fs"] } [dev-dependencies] divan = { workspace = true } diff --git a/src/uu/wc/src/count_fast.rs b/src/uu/wc/src/count_fast.rs index 5e2ecd080a0..9fe6f8145bc 100644 --- a/src/uu/wc/src/count_fast.rs +++ b/src/uu/wc/src/count_fast.rs @@ -16,8 +16,6 @@ use std::io::{self, ErrorKind, Read}; #[cfg(unix)] use libc::{_SC_PAGESIZE, S_IFREG, sysconf}; #[cfg(unix)] -use nix::sys::stat; -#[cfg(unix)] use std::io::{Seek, SeekFrom}; #[cfg(unix)] use std::os::fd::{AsFd, AsRawFd}; @@ -49,7 +47,9 @@ fn count_bytes_using_splice(fd: &impl AsFd) -> Result { .write(true) .open("/dev/null") .map_err(|_| 0_usize)?; - let null_rdev = stat::fstat(null_file.as_fd()).map_err(|_| 0_usize)?.st_rdev as libc::dev_t; + let null_rdev = rustix::fs::fstat(null_file.as_fd()) + .map_err(|_| 0_usize)? + .st_rdev as libc::dev_t; if (libc::major(null_rdev), libc::minor(null_rdev)) != (1, 3) { // This is not a proper /dev/null, writing to it is probably bad // Bit of an edge case, but it has been known to happen @@ -92,7 +92,7 @@ pub(crate) fn count_bytes_fast(handle: &mut T) -> (usize, Opti #[cfg(unix)] { let fd = handle.as_fd(); - if let Ok(stat) = stat::fstat(fd) { + if let Ok(stat) = rustix::fs::fstat(fd) { // If the file is regular, then the `st_size` should hold // the file's size in bytes. // If stat.st_size = 0 then diff --git a/src/uu/wc/src/wc.rs b/src/uu/wc/src/wc.rs index eaabbce8c28..5cc1bcd9017 100644 --- a/src/uu/wc/src/wc.rs +++ b/src/uu/wc/src/wc.rs @@ -297,11 +297,10 @@ impl<'a> Input<'a> { #[cfg(unix)] fn is_stdin_small_file() -> bool { - use nix::sys::stat; use std::os::fd::AsFd; matches!( - stat::fstat(io::stdin().as_fd()), + rustix::fs::fstat(io::stdin().as_fd()), Ok(meta) if meta.st_mode as libc::mode_t & libc::S_IFMT == libc::S_IFREG && meta.st_size <= (10 << 20) ) } From 2a8109299857f601c64a77142285d5c9426f73ff Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Tue, 10 Mar 2026 15:53:42 +0100 Subject: [PATCH 119/264] tests: ensure getlimits is on PATH for reused GNU builds --- util/build-gnu.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/util/build-gnu.sh b/util/build-gnu.sh index 3f6ec60d817..bd84e29a86a 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -160,6 +160,10 @@ else touch gnu-built fi +# Keep getlimits available on PATH for GNU shell and Perl tests even when +# reusing an existing GNU build directory. +test -f src/getlimits && cp -f src/getlimits "${UU_BUILD_DIR}" + # Keep Makefile.in newer than the local.mk files we just modified, # and Makefile newer than Makefile.in, so make won't re-run # automake or config.status and undo our edits. From f78bfb72c82bba3c20cda6b9b12d80dcfd78682b Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Wed, 1 Apr 2026 18:39:16 +0900 Subject: [PATCH 120/264] tee: remove Box and dyn --- src/uu/tee/src/tee.rs | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/src/uu/tee/src/tee.rs b/src/uu/tee/src/tee.rs index 344286aa662..6426e447d97 100644 --- a/src/uu/tee/src/tee.rs +++ b/src/uu/tee/src/tee.rs @@ -178,14 +178,12 @@ fn tee(options: &Options) -> Result<()> { 0, NamedWriter { name: translate!("tee-standard-output").into(), - inner: Box::new(stdout()), + inner: Writer::Stdout(stdout()), }, ); let mut output = MultiWriter::new(writers, options.output_error.clone()); - let input = &mut NamedReader { - inner: Box::new(stdin()) as Box, - }; + let input = NamedReader { inner: stdin() }; #[cfg(target_os = "linux")] if options.ignore_pipe_errors && !ensure_stdout_not_broken()? && output.writers.len() == 1 { @@ -263,7 +261,7 @@ fn open( }; match mode.write(true).create(true).open(path.as_path()) { Ok(file) => Some(Ok(NamedWriter { - inner: Box::new(file), + inner: Writer::File(file), name: name.clone(), })), Err(f) => { @@ -389,8 +387,29 @@ impl Write for MultiWriter { } } +enum Writer { + File(std::fs::File), + Stdout(std::io::Stdout), +} + +impl Write for Writer { + fn write(&mut self, buf: &[u8]) -> Result { + match self { + Self::File(f) => f.write(buf), + Self::Stdout(s) => s.write(buf), + } + } + + fn flush(&mut self) -> Result<()> { + match self { + Self::File(f) => f.flush(), + Self::Stdout(s) => s.flush(), + } + } +} + struct NamedWriter { - inner: Box, + inner: Writer, pub name: OsString, } @@ -405,7 +424,7 @@ impl Write for NamedWriter { } struct NamedReader { - inner: Box, + inner: std::io::Stdin, } impl Read for NamedReader { From 19b2d9b3c97b878b816722513126ffac05bdc6a3 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sat, 21 Mar 2026 19:41:26 +0100 Subject: [PATCH 121/264] sort: add locale-aware month sorting (-M) fix: tests/sort/sort-month.sh --- src/uu/sort/Cargo.toml | 2 + src/uu/sort/src/sort.rs | 125 ++++++++++----- src/uucore/src/lib/features/i18n/datetime.rs | 157 ++++++++++++++++++- tests/by-util/test_sort.rs | 111 +++++++++++++ 4 files changed, 358 insertions(+), 37 deletions(-) diff --git a/src/uu/sort/Cargo.toml b/src/uu/sort/Cargo.toml index c648763764f..d9bc86a687e 100644 --- a/src/uu/sort/Cargo.toml +++ b/src/uu/sort/Cargo.toml @@ -42,6 +42,7 @@ uucore = { workspace = true, features = [ "version-cmp", "i18n-decimal", "i18n-collator", + "i18n-datetime", ] } fluent = { workspace = true } foldhash = { workspace = true } @@ -61,6 +62,7 @@ uucore = { workspace = true, features = [ "parser-size", "version-cmp", "i18n-collator", + "i18n-datetime", ] } [[bin]] diff --git a/src/uu/sort/src/sort.rs b/src/uu/sort/src/sort.rs index 201267906bb..203bb30a111 100644 --- a/src/uu/sort/src/sort.rs +++ b/src/uu/sort/src/sort.rs @@ -8,6 +8,7 @@ // https://www.gnu.org/software/coreutils/manual/html_node/sort-invocation.html // spell-checker:ignore (misc) HFKJFK Mbdfhn getrlimit RLIMIT_NOFILE rlim bigdecimal extendedbigdecimal hexdigit behaviour keydef GETFD localeconv foldhash +// spell-checker:ignore (misc) uppercased qsort getmonth juin juil mod buffer_hint; mod check; @@ -42,6 +43,7 @@ use std::os::unix::ffi::OsStrExt; use std::path::Path; use std::path::PathBuf; use std::str::Utf8Error; +use std::sync::OnceLock; use thiserror::Error; use uucore::display::Quotable; use uucore::error::{FromIo, strip_errno}; @@ -49,6 +51,7 @@ use uucore::error::{UError, UResult, USimpleError, UUsageError}; use uucore::extendedbigdecimal::ExtendedBigDecimal; #[cfg(feature = "i18n-collator")] use uucore::i18n::collator::{compute_sort_key_utf8, locale_cmp}; +use uucore::i18n::datetime::get_locale_months; use uucore::i18n::decimal::locale_decimal_separator; use uucore::line_ending::LineEnding; use uucore::parser::num_parser::{ExtendedParser, ExtendedParserError}; @@ -787,31 +790,23 @@ impl<'a> Line<'a> { } SortMode::Month => { let initial_selection = &self.line[selection.clone()]; - - let mut month_chars = initial_selection + let first_non_blank = initial_selection .iter() - .enumerate() - .skip_while(|(_, c)| c.is_ascii_whitespace()); + .position(|c| !c.is_ascii_whitespace()) + .unwrap_or(initial_selection.len()); - let month = if month_parse(initial_selection) == Month::Unknown { + let (parsed, match_len) = month_parse(initial_selection); + + if parsed == Month::Unknown { // We failed to parse a month, which is equivalent to matching nothing. // Add the "no match for key" marker to the first non-whitespace character. - let first_non_whitespace = month_chars.next(); - first_non_whitespace.map_or( - initial_selection.len()..initial_selection.len(), - |(idx, _)| idx..idx, - ) + selection.start += first_non_blank; + selection.end = selection.start; } else { - // We parsed a month. Match the first three non-whitespace characters, which must be the month we parsed. - month_chars.next().unwrap().0 - ..month_chars - .nth(2) - .map_or(initial_selection.len(), |(idx, _)| idx) - }; - - // Shorten selection to month. - selection.start += month.start; - selection.end = selection.start + month.len(); + // We parsed a month. Use the actual match byte length. + selection.start += first_non_blank; + selection.end = selection.start + match_len; + } } _ => {} } @@ -3021,30 +3016,88 @@ enum Month { December, } +/// Cached locale month lookup table. +/// Each entry is (uppercased_name, month_value). +type MonthTable = Vec<(Vec, Month)>; + +fn get_locale_month_table() -> Option<&'static MonthTable> { + static TABLE: OnceLock> = OnceLock::new(); + + TABLE + .get_or_init(|| { + let months = get_locale_months()?; + let all_months = [ + Month::January, + Month::February, + Month::March, + Month::April, + Month::May, + Month::June, + Month::July, + Month::August, + Month::September, + Month::October, + Month::November, + Month::December, + ]; + let table: Vec<(Vec, Month)> = months + .iter() + .zip(all_months.iter()) + .map(|(name, &month)| (name.clone(), month)) + .collect(); + Some(table) + }) + .as_ref() +} + /// Parse the beginning string into a Month, returning [`Month::Unknown`] on errors. -fn month_parse(line: &[u8]) -> Month { +/// Also returns the byte length consumed from the input (after leading blanks). +/// +/// The stored locale month names have blanks stripped and are uppercased. +/// Comparison against input is case-insensitive but NOT blank-insensitive: +/// the input must match the stored name exactly (after leading blank trimming). +fn month_parse(line: &[u8]) -> (Month, usize) { let line = line.trim_ascii_start(); + // Try locale-specific month names, keeping the longest match. + // This handles cases where one name is a prefix of another + // (e.g., Japanese "1" vs "10", "11", "12"). + if let Some(table) = get_locale_month_table() { + let mut best = None; + for (name, month) in table { + if line.len() >= name.len() + && line[..name.len()].eq_ignore_ascii_case(name) + && best.as_ref().is_none_or(|&(len, _)| name.len() > len) + { + best = Some((name.len(), *month)); + } + } + if let Some((len, month)) = best { + return (month, len); + } + } + + // Fall back to English 3-letter abbreviations match line.get(..3).map(<[u8]>::to_ascii_uppercase).as_deref() { - Some(b"JAN") => Month::January, - Some(b"FEB") => Month::February, - Some(b"MAR") => Month::March, - Some(b"APR") => Month::April, - Some(b"MAY") => Month::May, - Some(b"JUN") => Month::June, - Some(b"JUL") => Month::July, - Some(b"AUG") => Month::August, - Some(b"SEP") => Month::September, - Some(b"OCT") => Month::October, - Some(b"NOV") => Month::November, - Some(b"DEC") => Month::December, - _ => Month::Unknown, + Some(b"JAN") => (Month::January, 3), + Some(b"FEB") => (Month::February, 3), + Some(b"MAR") => (Month::March, 3), + Some(b"APR") => (Month::April, 3), + Some(b"MAY") => (Month::May, 3), + Some(b"JUN") => (Month::June, 3), + Some(b"JUL") => (Month::July, 3), + Some(b"AUG") => (Month::August, 3), + Some(b"SEP") => (Month::September, 3), + Some(b"OCT") => (Month::October, 3), + Some(b"NOV") => (Month::November, 3), + Some(b"DEC") => (Month::December, 3), + _ => (Month::Unknown, 0), } } fn month_compare(a: &[u8], b: &[u8]) -> Ordering { - let ma = month_parse(a); - let mb = month_parse(b); + let ma = month_parse(a).0; + let mb = month_parse(b).0; ma.cmp(&mb) } diff --git a/src/uucore/src/lib/features/i18n/datetime.rs b/src/uucore/src/lib/features/i18n/datetime.rs index 88816d9daed..c79b4426fa9 100644 --- a/src/uucore/src/lib/features/i18n/datetime.rs +++ b/src/uucore/src/lib/features/i18n/datetime.rs @@ -3,7 +3,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore fieldsets prefs febr +// spell-checker:ignore fieldsets prefs febr abmon langinfo uppercased //! Locale-aware datetime formatting utilities using ICU and jiff-icu @@ -137,10 +137,165 @@ pub fn localize_format_string(format: &str, date: JiffDate) -> String { fmt.replace(PERCENT_PLACEHOLDER, "%%") } +/// Abbreviated month names for the current LC_TIME locale as raw bytes, +/// with blanks stripped and uppercased using ASCII case folding. +/// +/// Each entry corresponds to months January (index 0) through December (index 11). +/// Returns `None` for C/POSIX locale (caller should use English defaults). +/// This matches the GNU coreutils approach of storing uppercased, blank-stripped names. +pub fn get_locale_months() -> Option<&'static [Vec; 12]> { + static LOCALE_MONTHS: OnceLock; 12]>> = OnceLock::new(); + + LOCALE_MONTHS + .get_or_init(|| { + if !should_use_icu_locale() { + return None; + } + get_locale_months_inner() + }) + .as_ref() +} + +/// Unix implementation using nl_langinfo for exact match with `locale abmon` output. +#[cfg(any( + target_os = "linux", + target_vendor = "apple", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd", + target_os = "dragonfly" +))] +fn get_locale_months_inner() -> Option<[Vec; 12]> { + use nix::libc; + use std::ffi::CStr; + + let abmon_items: [libc::nl_item; 12] = [ + libc::ABMON_1, + libc::ABMON_2, + libc::ABMON_3, + libc::ABMON_4, + libc::ABMON_5, + libc::ABMON_6, + libc::ABMON_7, + libc::ABMON_8, + libc::ABMON_9, + libc::ABMON_10, + libc::ABMON_11, + libc::ABMON_12, + ]; + + // SAFETY: setlocale and nl_langinfo are standard POSIX functions. + // We call setlocale(LC_TIME, "") to initialize from environment variables, + // then read the abbreviated month names. This is called once (via OnceLock) + // and cached, so the race window with other setlocale callers is minimal. + // The nl_langinfo return pointer is immediately copied below. + unsafe { + libc::setlocale(libc::LC_TIME, c"".as_ptr()); + } + + let mut months: [Vec; 12] = Default::default(); + for (i, &item) in abmon_items.iter().enumerate() { + // SAFETY: nl_langinfo returns a valid C string pointer for valid nl_item values. + let ptr = unsafe { libc::nl_langinfo(item) }; + if ptr.is_null() { + return None; + } + let name = unsafe { CStr::from_ptr(ptr) }.to_bytes(); + if name.is_empty() { + return None; + } + // Strip blanks and uppercase using ASCII case folding, matching GNU behavior + months[i] = name + .iter() + .filter(|&&b| !b.is_ascii_whitespace()) + .map(|&b| b.to_ascii_uppercase()) + .collect(); + } + + Some(months) +} + +/// Non-Unix fallback using ICU DateTimeFormatter. +#[cfg(not(any( + target_os = "linux", + target_vendor = "apple", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd", + target_os = "dragonfly" +)))] +fn get_locale_months_inner() -> Option<[Vec; 12]> { + let (locale, _) = get_time_locale(); + let locale_prefs = locale.clone().into(); + // M::medium() produces abbreviated month names (e.g. "Jan", "Feb") matching + // nl_langinfo(ABMON_*) on Unix. M::short() produces numeric ("1", "2") and + // M::long() produces full names ("January", "February"). + let formatter = DateTimeFormatter::try_new(locale_prefs, fieldsets::M::medium()).ok()?; + + let mut months: [Vec; 12] = Default::default(); + for i in 0..12u8 { + let iso_date = Date::::try_new_iso(2000, i + 1, 1).ok()?; + let formatted = formatter.format(&iso_date).to_string(); + // Strip blanks and uppercase using ASCII case folding + months[i as usize] = formatted + .bytes() + .filter(|b| !b.is_ascii_whitespace()) + .map(|b| b.to_ascii_uppercase()) + .collect(); + } + + Some(months) +} + #[cfg(test)] mod tests { use super::*; + /// Verify that ICU `M::medium()` produces abbreviated month names matching + /// what `nl_langinfo(ABMON_*)` returns on Unix. This is the format used by + /// the non-Unix fallback in `get_locale_months_inner`. + #[test] + fn test_icu_medium_month_produces_abbreviated_names() { + use icu_locale::locale; + + let locale: Locale = locale!("en-US"); + let formatter = DateTimeFormatter::try_new(locale.into(), fieldsets::M::medium()).unwrap(); + + let expected = [ + "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", + ]; + + for (i, exp) in expected.iter().enumerate() { + let iso_date = Date::::try_new_iso(2000, (i + 1) as u8, 1).unwrap(); + let formatted = formatter.format(&iso_date).to_string(); + assert_eq!( + &formatted, + exp, + "M::medium() for month {} should produce abbreviated name", + i + 1 + ); + } + } + + /// Confirm that M::short() gives numeric months and M::long() gives full names, + /// so M::medium() is the only correct choice for abbreviated month names. + #[test] + fn test_icu_short_and_long_month_formats_differ() { + use icu_locale::locale; + + let locale: Locale = locale!("en-US"); + let iso_jan = Date::::try_new_iso(2000, 1, 1).unwrap(); + + let short_fmt = + DateTimeFormatter::try_new(locale.clone().into(), fieldsets::M::short()).unwrap(); + let long_fmt = DateTimeFormatter::try_new(locale.into(), fieldsets::M::long()).unwrap(); + + // M::short() produces numeric ("1"), not "Jan" + assert_eq!(short_fmt.format(&iso_jan).to_string(), "1"); + // M::long() produces full name ("January"), not "Jan" + assert_eq!(long_fmt.format(&iso_jan).to_string(), "January"); + } + #[test] fn test_calendar_type_detection() { use icu_locale::locale; diff --git a/tests/by-util/test_sort.rs b/tests/by-util/test_sort.rs index ea4abed17a0..7657e79ba88 100644 --- a/tests/by-util/test_sort.rs +++ b/tests/by-util/test_sort.rs @@ -603,6 +603,117 @@ fn test_month_default2() { } } +#[test] +fn test_month_sort_french_locale() { + // Test locale-aware month sorting (mirrors GNU sort-month.sh test). + // Uses French UTF-8 locale where abbreviated months are: + // spell-checker:disable-next-line + // janv. févr. mars avril mai juin juil. août sept. oct. nov. déc. + let result = new_ucmd!() + .env("LC_ALL", "fr_FR.UTF-8") + .arg("-M") + // spell-checker:disable-next-line + .pipe_in("mai\ndéc.\njanv.\njuin\nfévr.\nmars\navril\njuil.\naoût\nsept.\noct.\nnov.\n") + .run(); + + // Skip if locale is not available on the system + if result.succeeded() { + assert_eq!( + result.stdout_str(), + "janv.\nfévr.\nmars\navril\nmai\njuin\njuil.\naoût\nsept.\noct.\nnov.\ndéc.\n", + ); + } +} + +#[test] +fn test_month_sort_hungarian_locale() { + // Hungarian abbreviated months: jan febr márc ápr máj jún júl aug szept okt nov dec + let result = new_ucmd!() + .env("LC_ALL", "hu_HU.UTF-8") + .arg("-M") + // spell-checker:disable-next-line + .pipe_in("máj\ndec\njan\njún\nfebr\nmárc\nápr\njúl\naug\nszept\nokt\nnov\n") + .run(); + + if result.succeeded() { + assert_eq!( + result.stdout_str(), + "jan\nfebr\nmárc\nápr\nmáj\njún\njúl\naug\nszept\nokt\nnov\ndec\n", + ); + } +} + +/// Test that embedded blanks in month names cause a non-match (GNU compat). +/// E.g. "av ril" should NOT match "avril" — GNU treats it as unknown. +#[test] +#[cfg(unix)] +fn test_month_sort_french_embedded_blanks() { + let locale = "fr_FR.UTF-8"; + if !is_locale_available(locale) { + return; + } + // spell-checker:disable + // Pick three locale months (indices 2=March, 3=April, 5=June) and verify + // that inserting blanks into April's name causes a non-match. + // On glibc these are "mars", "avril", "juin"; on other systems they vary. + #[cfg(any(target_vendor = "apple", target_os = "openbsd"))] + let months = { + let Some(m) = get_system_abmon(locale) else { + return; + }; + m + }; + #[cfg(not(any(target_vendor = "apple", target_os = "openbsd")))] + let months = vec![ + "janv.", "févr.", "mars", "avril", "mai", "juin", "juil.", "août", "sept.", "oct.", "nov.", + "déc.", + ] + .into_iter() + .map(String::from) + .collect::>(); + + let march = &months[2]; + let april = &months[3]; + let june = &months[5]; + + // Build a mangled version of April with embedded spaces: e.g. "avril" -> "av ril" + // Insert spaces after the second byte. + if april.len() < 3 { + // Month name too short to meaningfully insert blanks; skip. + return; + } + let mangled = format!("{} {}", &april[..2], &april[2..]); + + // Input: june, mangled-april, march + let input = format!("{june}\n{mangled}\n{march}\n"); + // Expected: mangled sorts as unknown (first), then march (3), then june (6) + let expected = format!("{mangled}\n{march}\n{june}\n"); + // spell-checker:enable + new_ucmd!() + .env("LC_ALL", locale) + .arg("-M") + .pipe_in(input) + .succeeds() + .stdout_is(expected); +} + +#[test] +fn test_month_sort_japanese_locale() { + // Japanese abbreviated months have leading spaces: " 1月" " 2月" ... "10月" "11月" "12月" + let result = new_ucmd!() + .env("LC_ALL", "ja_JP.UTF-8") + .arg("-M") + .pipe_in("5月\n12月\n1月\n6月\n2月\n3月\n4月\n7月\n8月\n9月\n10月\n11月\n") + .run(); + + if result.succeeded() { + assert_eq!( + result.stdout_str(), + "1月\n2月\n3月\n4月\n5月\n6月\n7月\n8月\n9月\n10月\n11月\n12月\n", + ); + } +} + #[test] fn test_default_unsorted_ints2() { let input = "9\n1909888\n000\n1\n2"; From aa1c61c6b5022b3bbe54a617d4e23df21aa04c9e Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sat, 21 Mar 2026 23:09:48 +0100 Subject: [PATCH 122/264] tests: deduplicate locale availability checks into shared is_locale_available() --- tests/by-util/test_date.rs | 47 ++-------- tests/by-util/test_sort.rs | 162 +++++++++++++++++++++++++--------- tests/uutests/src/lib/util.rs | 13 +++ 3 files changed, 137 insertions(+), 85 deletions(-) diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index db4637e6273..a6139848c08 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -13,6 +13,8 @@ use regex::Regex; #[cfg(all(unix, not(target_os = "macos")))] use uucore::process::geteuid; use uutests::util::TestScenario; +#[cfg(unix)] +use uutests::util::is_locale_available; use uutests::{at_and_ucmd, new_ucmd, util_name}; #[test] @@ -1840,20 +1842,7 @@ fn test_date_parenthesis_vs_other_special_chars() { fn test_date_iranian_locale_solar_hijri_calendar() { // Test Iranian locale uses Solar Hijri calendar // Verify the Solar Hijri calendar is used in the Iranian locale - use std::process::Command; - - // Check if Iranian locale is available - let locale_check = Command::new("locale") - .env("LC_ALL", "fa_IR.UTF-8") - .arg("charmap") - .output(); - - let locale_available = match locale_check { - Ok(output) => String::from_utf8_lossy(&output.stdout).trim() == "UTF-8", - Err(_) => false, - }; - - if !locale_available { + if !is_locale_available("fa_IR.UTF-8") { println!("Skipping Iranian locale test - fa_IR.UTF-8 locale not available"); return; } @@ -1920,20 +1909,7 @@ fn test_date_iranian_locale_solar_hijri_calendar() { fn test_date_ethiopian_locale_calendar() { // Test Ethiopian locale uses Ethiopian calendar // Verify the Ethiopian calendar is used in the Ethiopian locale - use std::process::Command; - - // Check if Ethiopian locale is available - let locale_check = Command::new("locale") - .env("LC_ALL", "am_ET.UTF-8") - .arg("charmap") - .output(); - - let locale_available = match locale_check { - Ok(output) => String::from_utf8_lossy(&output.stdout).trim() == "UTF-8", - Err(_) => false, - }; - - if !locale_available { + if !is_locale_available("am_ET.UTF-8") { println!("Skipping Ethiopian locale test - am_ET.UTF-8 locale not available"); return; } @@ -2000,20 +1976,7 @@ fn test_date_ethiopian_locale_calendar() { fn test_date_thai_locale_solar_calendar() { // Test Thai locale uses Thai solar calendar // Verify the Thai solar calendar is used with the Thai locale - use std::process::Command; - - // Check if Thai locale is available - let locale_check = Command::new("locale") - .env("LC_ALL", "th_TH.UTF-8") - .arg("charmap") - .output(); - - let locale_available = match locale_check { - Ok(output) => String::from_utf8_lossy(&output.stdout).trim() == "UTF-8", - Err(_) => false, - }; - - if !locale_available { + if !is_locale_available("th_TH.UTF-8") { println!("Skipping Thai locale test - th_TH.UTF-8 locale not available"); return; } diff --git a/tests/by-util/test_sort.rs b/tests/by-util/test_sort.rs index 7657e79ba88..b0e7602fd7f 100644 --- a/tests/by-util/test_sort.rs +++ b/tests/by-util/test_sort.rs @@ -3,7 +3,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (words) ints (linux) NOFILE dfgi +// spell-checker:ignore (words) ints (linux) NOFILE dfgi abmon avril #![allow(clippy::cast_possible_wrap)] use std::env; @@ -15,6 +15,8 @@ use std::time::Duration; use uutests::at_and_ucmd; use uutests::new_ucmd; use uutests::util::TestScenario; +#[cfg(unix)] +use uutests::util::is_locale_available; fn test_helper(file_name: &str, possible_args: &[&str]) { for args in possible_args { @@ -603,44 +605,106 @@ fn test_month_default2() { } } +/// Query the system for abbreviated month names via `locale abmon`. +/// Returns a vector of 12 month abbreviations in order (Jan..Dec), +/// or None if the command fails or returns unexpected output. +#[cfg(any(target_vendor = "apple", target_os = "openbsd"))] +fn get_system_abmon(locale: &str) -> Option> { + let output = Command::new("locale") + .env("LC_ALL", locale) + .arg("abmon") + .output() + .ok()?; + if !output.status.success() { + return None; + } + let text = String::from_utf8(output.stdout).ok()?; + let months: Vec = text + .trim() + .split(';') + .map(String::from) + .filter(|m| !m.is_empty()) + .collect(); + if months.len() == 12 { + Some(months) + } else { + None + } +} + +/// Build shuffled input and sorted expected output from month names. +#[cfg(any(target_vendor = "apple", target_os = "openbsd"))] +fn month_sort_input_expected(months: &[String]) -> (String, String) { + // Shuffled order: May, Dec, Jan, Jun, Feb, Mar, Apr, Jul, Aug, Sep, Oct, Nov + let shuffle_order = [4, 11, 0, 5, 1, 2, 3, 6, 7, 8, 9, 10]; + let input = shuffle_order + .iter() + .map(|&i| months[i].as_str()) + .collect::>() + .join("\n") + + "\n"; + let expected = months.join("\n") + "\n"; + (input, expected) +} + #[test] +#[cfg(unix)] fn test_month_sort_french_locale() { - // Test locale-aware month sorting (mirrors GNU sort-month.sh test). - // Uses French UTF-8 locale where abbreviated months are: - // spell-checker:disable-next-line - // janv. févr. mars avril mai juin juil. août sept. oct. nov. déc. - let result = new_ucmd!() - .env("LC_ALL", "fr_FR.UTF-8") - .arg("-M") - // spell-checker:disable-next-line - .pipe_in("mai\ndéc.\njanv.\njuin\nfévr.\nmars\navril\njuil.\naoût\nsept.\noct.\nnov.\n") - .run(); - - // Skip if locale is not available on the system - if result.succeeded() { - assert_eq!( - result.stdout_str(), - "janv.\nfévr.\nmars\navril\nmai\njuin\njuil.\naoût\nsept.\noct.\nnov.\ndéc.\n", - ); + let locale = "fr_FR.UTF-8"; + if !is_locale_available(locale) { + return; } + // spell-checker:disable + // On macOS/OpenBSD, abbreviated month names vary across OS versions (different CLDR data), + // so we query the system dynamically. On other platforms, glibc values are stable. + #[cfg(any(target_vendor = "apple", target_os = "openbsd"))] + let (input, expected) = { + let Some(months) = get_system_abmon(locale) else { + return; + }; + month_sort_input_expected(&months) + }; + #[cfg(not(any(target_vendor = "apple", target_os = "openbsd")))] + let (input, expected) = ( + "mai\ndéc.\njanv.\njuin\nfévr.\nmars\navril\njuil.\naoût\nsept.\noct.\nnov.\n".to_string(), + "janv.\nfévr.\nmars\navril\nmai\njuin\njuil.\naoût\nsept.\noct.\nnov.\ndéc.\n".to_string(), + ); + // spell-checker:enable + new_ucmd!() + .env("LC_ALL", locale) + .arg("-M") + .pipe_in(input) + .succeeds() + .stdout_is(expected); } #[test] +#[cfg(unix)] fn test_month_sort_hungarian_locale() { - // Hungarian abbreviated months: jan febr márc ápr máj jún júl aug szept okt nov dec - let result = new_ucmd!() - .env("LC_ALL", "hu_HU.UTF-8") - .arg("-M") - // spell-checker:disable-next-line - .pipe_in("máj\ndec\njan\njún\nfebr\nmárc\nápr\njúl\naug\nszept\nokt\nnov\n") - .run(); - - if result.succeeded() { - assert_eq!( - result.stdout_str(), - "jan\nfebr\nmárc\nápr\nmáj\njún\njúl\naug\nszept\nokt\nnov\ndec\n", - ); + let locale = "hu_HU.UTF-8"; + if !is_locale_available(locale) { + return; } + // spell-checker:disable + #[cfg(any(target_vendor = "apple", target_os = "openbsd"))] + let (input, expected) = { + let Some(months) = get_system_abmon(locale) else { + return; + }; + month_sort_input_expected(&months) + }; + #[cfg(not(any(target_vendor = "apple", target_os = "openbsd")))] + let (input, expected) = ( + "máj\ndec\njan\njún\nfebr\nmárc\nápr\njúl\naug\nszept\nokt\nnov\n".to_string(), + "jan\nfebr\nmárc\nápr\nmáj\njún\njúl\naug\nszept\nokt\nnov\ndec\n".to_string(), + ); + // spell-checker:enable + new_ucmd!() + .env("LC_ALL", locale) + .arg("-M") + .pipe_in(input) + .succeeds() + .stdout_is(expected); } /// Test that embedded blanks in month names cause a non-match (GNU compat). @@ -698,20 +762,32 @@ fn test_month_sort_french_embedded_blanks() { } #[test] +#[cfg(unix)] fn test_month_sort_japanese_locale() { - // Japanese abbreviated months have leading spaces: " 1月" " 2月" ... "10月" "11月" "12月" - let result = new_ucmd!() - .env("LC_ALL", "ja_JP.UTF-8") - .arg("-M") - .pipe_in("5月\n12月\n1月\n6月\n2月\n3月\n4月\n7月\n8月\n9月\n10月\n11月\n") - .run(); - - if result.succeeded() { - assert_eq!( - result.stdout_str(), - "1月\n2月\n3月\n4月\n5月\n6月\n7月\n8月\n9月\n10月\n11月\n12月\n", - ); + let locale = "ja_JP.UTF-8"; + if !is_locale_available(locale) { + return; } + // On macOS/OpenBSD, abbreviated month names may differ, so query dynamically. + #[cfg(any(target_vendor = "apple", target_os = "openbsd"))] + let (input, expected) = { + let Some(months) = get_system_abmon(locale) else { + return; + }; + month_sort_input_expected(&months) + }; + // Japanese abbreviated months are numeric (1月..12月) on glibc + #[cfg(not(any(target_vendor = "apple", target_os = "openbsd")))] + let (input, expected) = ( + "5月\n12月\n1月\n6月\n2月\n3月\n4月\n7月\n8月\n9月\n10月\n11月\n".to_string(), + "1月\n2月\n3月\n4月\n5月\n6月\n7月\n8月\n9月\n10月\n11月\n12月\n".to_string(), + ); + new_ucmd!() + .env("LC_ALL", locale) + .arg("-M") + .pipe_in(input) + .succeeds() + .stdout_is(expected); } #[test] diff --git a/tests/uutests/src/lib/util.rs b/tests/uutests/src/lib/util.rs index 16258400710..6e5e9690d24 100644 --- a/tests/uutests/src/lib/util.rs +++ b/tests/uutests/src/lib/util.rs @@ -100,6 +100,19 @@ pub fn is_ci() -> bool { env::var("CI").is_ok_and(|s| s.eq_ignore_ascii_case("true")) } +/// Check if a locale is available on the system by verifying that +/// `locale charmap` returns `"UTF-8"` when `LC_ALL` is set to the given locale. +#[cfg(unix)] +pub fn is_locale_available(locale: &str) -> bool { + use std::process::Command; + Command::new("locale") + .env("LC_ALL", locale) + .arg("charmap") + .output() + .map(|o| String::from_utf8_lossy(&o.stdout).trim() == "UTF-8") + .unwrap_or(false) +} + /// Read a test scenario fixture, returning its bytes fn read_scenario_fixture>(tmpd: Option<&Rc>, file_rel_path: S) -> Vec { let tmpdir_path = tmpd.as_ref().unwrap().as_ref().path(); From a9672e0371548bf71eb28e6c899ca1395633920e Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Mon, 30 Mar 2026 11:36:06 +0200 Subject: [PATCH 123/264] uucore: add WASI support for FileInformation, fs, and io modules Add #[cfg(target_os = "wasi")] branches to support building uucore for the wasm32-wasip1 target: - FileInformation: use std::fs::Metadata instead of nix::sys::stat - is_stdin_directory: return false (stdin is never a directory on WASI) - sane_blksize: use default block size (no MetadataExt on WASI) - read_fs_list: return empty list (no mount info on WASI) - into_stdio: convert via File since Stdio::from(OwnedFd) is unavailable --- src/uucore/src/lib/features/fs.rs | 45 ++++++++++++++++++++++++++-- src/uucore/src/lib/features/fsext.rs | 5 ++-- src/uucore/src/lib/mods/io.rs | 7 +++++ 3 files changed, 52 insertions(+), 5 deletions(-) diff --git a/src/uucore/src/lib/features/fs.rs b/src/uucore/src/lib/features/fs.rs index 6ef229d175c..bc7f92dd78b 100644 --- a/src/uucore/src/lib/features/fs.rs +++ b/src/uucore/src/lib/features/fs.rs @@ -45,6 +45,8 @@ macro_rules! has { pub struct FileInformation( #[cfg(unix)] nix::sys::stat::FileStat, #[cfg(windows)] winapi_util::file::Information, + // WASI does not have nix::sys::stat, so we store std::fs::Metadata instead. + #[cfg(target_os = "wasi")] std::fs::Metadata, ); impl FileInformation { @@ -91,6 +93,16 @@ impl FileInformation { let file = open_options.read(true).open(path.as_ref())?; Self::from_file(&file) } + // WASI: use std::fs::metadata / symlink_metadata since nix is not available + #[cfg(target_os = "wasi")] + { + let metadata = if dereference { + std::fs::metadata(path.as_ref()) + } else { + std::fs::symlink_metadata(path.as_ref()) + }; + Ok(Self(metadata?)) + } } pub fn file_size(&self) -> u64 { @@ -103,6 +115,10 @@ impl FileInformation { { self.0.file_size() } + #[cfg(target_os = "wasi")] + { + self.0.len() + } } #[cfg(windows)] @@ -153,6 +169,9 @@ impl FileInformation { return self.0.st_nlink.try_into().unwrap(); #[cfg(windows)] return self.0.number_of_links(); + // WASI: nlink is not available in std::fs::Metadata, return 1 + #[cfg(target_os = "wasi")] + return 1; } #[cfg(unix)] @@ -172,6 +191,15 @@ impl PartialEq for FileInformation { } } +// WASI: compare by file type and size as a basic heuristic since +// device/inode numbers are not available through std::fs::Metadata. +#[cfg(target_os = "wasi")] +impl PartialEq for FileInformation { + fn eq(&self, other: &Self) -> bool { + self.0.file_type() == other.0.file_type() && self.0.len() == other.0.len() + } +} + #[cfg(target_os = "windows")] impl PartialEq for FileInformation { fn eq(&self, other: &Self) -> bool { @@ -194,6 +222,10 @@ impl Hash for FileInformation { self.0.volume_serial_number().hash(state); self.0.file_index().hash(state); } + #[cfg(target_os = "wasi")] + { + self.0.len().hash(state); + } } } @@ -778,11 +810,18 @@ pub fn is_stdin_directory(stdin: &Stdin) -> bool { } false } + + // WASI: stdin is never a directory + #[cfg(target_os = "wasi")] + { + let _ = stdin; + false + } } pub mod sane_blksize { - #[cfg(not(target_os = "windows"))] + #[cfg(unix)] use std::os::unix::fs::MetadataExt; use std::{fs::metadata, path::Path}; @@ -809,12 +848,12 @@ pub mod sane_blksize { #[cfg(unix)] metadata: &std::fs::Metadata, #[cfg(not(unix))] _: &std::fs::Metadata, ) -> u64 { - #[cfg(not(target_os = "windows"))] + #[cfg(unix)] { sane_blksize(metadata.blksize()) } - #[cfg(target_os = "windows")] + #[cfg(not(unix))] { DEFAULT } diff --git a/src/uucore/src/lib/features/fsext.rs b/src/uucore/src/lib/features/fsext.rs index 5eacc7a4c49..26070b108de 100644 --- a/src/uucore/src/lib/features/fsext.rs +++ b/src/uucore/src/lib/features/fsext.rs @@ -535,10 +535,11 @@ pub fn read_fs_list() -> UResult> { target_os = "aix", target_os = "redox", target_os = "illumos", - target_os = "solaris" + target_os = "solaris", + target_os = "wasi" ))] { - // No method to read mounts, yet + // No method to read mounts on these platforms Ok(Vec::new()) } } diff --git a/src/uucore/src/lib/mods/io.rs b/src/uucore/src/lib/mods/io.rs index c83c3d98744..a9e4a7bcbec 100644 --- a/src/uucore/src/lib/mods/io.rs +++ b/src/uucore/src/lib/mods/io.rs @@ -73,10 +73,17 @@ impl OwnedFileDescriptorOrHandle { } /// instantiates a corresponding `Stdio` + #[cfg(not(target_os = "wasi"))] pub fn into_stdio(self) -> Stdio { Stdio::from(self.fx) } + /// WASI: Stdio::from(OwnedFd) is not available, convert via File instead. + #[cfg(target_os = "wasi")] + pub fn into_stdio(self) -> Stdio { + Stdio::from(File::from(self.fx)) + } + /// clones self. useful when needing another /// owned reference to same file pub fn try_clone(&self) -> io::Result { From b48da6854b4f051c9135e8d2a62ea4d585528195 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Mon, 30 Mar 2026 11:37:24 +0200 Subject: [PATCH 124/264] ls: make hostname detection return nothing on WASI target The hostname crate does not support WASI (no OS-level hostname API). --- src/uu/ls/Cargo.toml | 5 ++++- src/uu/ls/src/display.rs | 5 +++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/uu/ls/Cargo.toml b/src/uu/ls/Cargo.toml index a82ad13c397..b8688819ef8 100644 --- a/src/uu/ls/Cargo.toml +++ b/src/uu/ls/Cargo.toml @@ -24,7 +24,6 @@ path = "src/ls.rs" ansi-width = { workspace = true } clap = { workspace = true, features = ["env"] } glob = { workspace = true } -hostname = { workspace = true } lscolors = { workspace = true } rustc-hash = { workspace = true } selinux = { workspace = true, optional = true } @@ -46,6 +45,10 @@ uucore = { workspace = true, features = [ uutils_term_grid = { workspace = true } fluent = { workspace = true } +# hostname crate does not support WASI (no OS-level hostname API) +[target.'cfg(not(target_os = "wasi"))'.dependencies] +hostname = { workspace = true } + [[bin]] name = "ls" path = "src/main.rs" diff --git a/src/uu/ls/src/display.rs b/src/uu/ls/src/display.rs index 929ce7e4610..d1c35e45c7a 100644 --- a/src/uu/ls/src/display.rs +++ b/src/uu/ls/src/display.rs @@ -1146,7 +1146,12 @@ fn classify_file(path: &PathData) -> Option { } fn create_hyperlink(name: &OsStr, path: &PathData) -> OsString { + // The `hostname` crate does not support WASI (no OS-level hostname API), + // so we use an empty string for hyperlinks on WASI. + #[cfg(not(target_os = "wasi"))] static HOSTNAME: LazyLock = LazyLock::new(|| hostname::get().unwrap_or_default()); + #[cfg(target_os = "wasi")] + static HOSTNAME: LazyLock = LazyLock::new(OsString::new); // OSC 8 hyperlink format: \x1b]8;;URL\x1b\\TEXT\x1b]8;;\x1b\\ // \x1b = ESC, \x1b\\ = ESC backslash From 9cd11a0dd558cfaef6434f352f9c029fd4f735ee Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Mon, 30 Mar 2026 11:39:58 +0200 Subject: [PATCH 125/264] feat_wasm: add ls to the WASM-compatible utilities Now that hostname is optional and uucore has WASI stubs, ls can be built for the wasm32-wasip1 target. --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index 4f98541268e..9cdfab875e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -185,6 +185,7 @@ feat_wasm = [ "base64", "basenc", "cut", + "ls", "date", "dircolors", "dirname", From 71be16174c42af9a154f8b753e5580bf4e5de356 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Wed, 25 Mar 2026 15:40:38 +0900 Subject: [PATCH 126/264] tee: fix < . message and strip errno --- src/uu/tee/locales/en-US.ftl | 2 +- src/uu/tee/locales/fr-FR.ftl | 2 +- src/uu/tee/src/tee.rs | 8 ++++++-- tests/by-util/test_tee.rs | 9 +++++++++ 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/uu/tee/locales/en-US.ftl b/src/uu/tee/locales/en-US.ftl index 4db948e6c3b..47bd49c74a0 100644 --- a/src/uu/tee/locales/en-US.ftl +++ b/src/uu/tee/locales/en-US.ftl @@ -14,7 +14,7 @@ tee-help-output-error-exit = exit on write errors to any output tee-help-output-error-exit-nopipe = exit on write errors to any output that are not pipe errors (equivalent to exit on non-unix platforms) # Error messages -tee-error-stdin = stdin: { $error } +tee-error-stdin = read error: { $error } # Other messages tee-standard-output = 'standard output' diff --git a/src/uu/tee/locales/fr-FR.ftl b/src/uu/tee/locales/fr-FR.ftl index eeea09ac492..e86faf9b6df 100644 --- a/src/uu/tee/locales/fr-FR.ftl +++ b/src/uu/tee/locales/fr-FR.ftl @@ -14,7 +14,7 @@ tee-help-output-error-exit = quitter en cas d'erreurs d'écriture vers toute sor tee-help-output-error-exit-nopipe = quitter en cas d'erreurs d'écriture vers toute sortie qui ne sont pas des erreurs de tube (équivalent à exit sur les plateformes non-unix) # Messages d'erreur -tee-error-stdin = stdin : { $error } +tee-error-stdin = erreur de lecture: { $error } # Autres messages tee-standard-output = 'sortie standard' diff --git a/src/uu/tee/src/tee.rs b/src/uu/tee/src/tee.rs index 6426e447d97..5db116df4e8 100644 --- a/src/uu/tee/src/tee.rs +++ b/src/uu/tee/src/tee.rs @@ -9,7 +9,7 @@ use std::fs::OpenOptions; use std::io::{Error, ErrorKind, Read, Result, Write, stderr, stdin, stdout}; use std::path::PathBuf; use uucore::display::Quotable; -use uucore::error::UResult; +use uucore::error::{UResult, strip_errno}; use uucore::format_usage; use uucore::parser::shortcut_value_parser::ShortcutValueParser; use uucore::translate; @@ -431,7 +431,11 @@ impl Read for NamedReader { fn read(&mut self, buf: &mut [u8]) -> Result { match self.inner.read(buf) { Err(f) => { - let _ = writeln!(stderr(), "{}", translate!("tee-error-stdin", "error" => f)); + let _ = writeln!( + stderr(), + "tee: {}", + translate!("tee-error-stdin", "error" => strip_errno(&f)) + ); Err(f) } okay => okay, diff --git a/tests/by-util/test_tee.rs b/tests/by-util/test_tee.rs index 8d03328b6f5..ae3e34dcd14 100644 --- a/tests/by-util/test_tee.rs +++ b/tests/by-util/test_tee.rs @@ -16,6 +16,15 @@ use std::time::Duration; // spell-checker:ignore nopipe +#[test] +#[cfg(unix)] +fn test_error_stdin_directory() { + new_ucmd!() + .set_stdin(std::fs::File::open(".").unwrap()) + .fails_with_code(1) + .stderr_is("tee: read error: Is a directory\n"); +} + #[test] fn test_invalid_arg() { new_ucmd!().arg("--definitely-invalid").fails_with_code(1); From ecba81653bbc656b7471cbb61c2602dcee8fdea7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 20:26:51 +0000 Subject: [PATCH 127/264] chore(deps): update rust crate zip to v8.5.0 --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b340ac549ab..c48c15c51e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5226,9 +5226,9 @@ dependencies = [ [[package]] name = "zip" -version = "8.4.0" +version = "8.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7756d0206d058333667493c4014f545f4b9603c4330ccd6d9b3f86dcab59f7d9" +checksum = "2726508a48f38dceb22b35ecbbd2430efe34ff05c62bd3285f965d7911b33464" dependencies = [ "crc32fast", "flate2", From 298c47ac68273aeebf8ef5afdabd05b58dc4a576 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 20:26:43 +0000 Subject: [PATCH 128/264] chore(deps): update rust crate icu_provider to v2.2.0 --- Cargo.lock | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c48c15c51e5..c0b7cb3b3b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1508,9 +1508,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -1603,9 +1603,9 @@ checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" [[package]] name = "icu_provider" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", @@ -3058,9 +3058,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "serde_core", @@ -5101,9 +5101,9 @@ checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" [[package]] name = "yoke" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -5112,9 +5112,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", @@ -5192,20 +5192,21 @@ dependencies = [ [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", "zerofrom", + "zerovec", ] [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "serde", "yoke", @@ -5215,9 +5216,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", From e0a770e0f733a196b21f458d6a3d739628fae0d8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 05:10:24 +0000 Subject: [PATCH 129/264] fix(deps): update rust crate similar to v3 --- fuzz/Cargo.lock | 21 ++++++++++++--------- fuzz/uufuzz/Cargo.toml | 2 +- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index da97b02dc8d..8879c35b585 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -62,7 +62,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -73,7 +73,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -582,7 +582,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1125,7 +1125,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde_core", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1620,7 +1620,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1730,9 +1730,12 @@ checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" [[package]] name = "similar" -version = "2.7.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" +checksum = "26d0b06eba54f0ca0770f970a3e89823e766ca638dd940f8469fa0fa50c75396" +dependencies = [ + "bstr", +] [[package]] name = "sm3" @@ -1799,7 +1802,7 @@ dependencies = [ "getrandom 0.4.1", "once_cell", "rustix", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2333,7 +2336,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] diff --git a/fuzz/uufuzz/Cargo.toml b/fuzz/uufuzz/Cargo.toml index 39f24fd20c5..2ca4ff9f3dd 100644 --- a/fuzz/uufuzz/Cargo.toml +++ b/fuzz/uufuzz/Cargo.toml @@ -12,6 +12,6 @@ license.workspace = true console = "0.16.0" libc = "0.2.153" rand = { version = "0.9.0", features = ["small_rng"] } -similar = "2.5.0" +similar = "3.0.0" uucore = { version = "0.8.0", path = "../../src/uucore", features = ["parser"] } tempfile = "3.15.0" From 30263048f5a42700cc1299c28b9ad866b86abade Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Thu, 2 Apr 2026 00:17:22 +0900 Subject: [PATCH 130/264] tee: simplify by clap --- src/uu/tee/src/tee.rs | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/src/uu/tee/src/tee.rs b/src/uu/tee/src/tee.rs index 5db116df4e8..dd584b571f8 100644 --- a/src/uu/tee/src/tee.rs +++ b/src/uu/tee/src/tee.rs @@ -57,24 +57,16 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let append = matches.get_flag(options::APPEND); let ignore_interrupts = matches.get_flag(options::IGNORE_INTERRUPTS); let ignore_pipe_errors = matches.get_flag(options::IGNORE_PIPE_ERRORS); - let output_error = if matches.contains_id(options::OUTPUT_ERROR) { - match matches - .get_one::(options::OUTPUT_ERROR) - .map(String::as_str) - { - Some("warn") => Some(OutputErrorMode::Warn), - // If no argument is specified for --output-error, - // defaults to warn-nopipe - None | Some("warn-nopipe") => Some(OutputErrorMode::WarnNoPipe), - Some("exit") => Some(OutputErrorMode::Exit), - Some("exit-nopipe") => Some(OutputErrorMode::ExitNoPipe), - _ => unreachable!(), - } - } else if ignore_pipe_errors { - Some(OutputErrorMode::WarnNoPipe) - } else { - None - }; + let output_error = matches + .get_one::(options::OUTPUT_ERROR) + .map(|s| match s.as_str() { + "warn" => OutputErrorMode::Warn, + "warn-nopipe" => OutputErrorMode::WarnNoPipe, + "exit" => OutputErrorMode::Exit, + "exit-nopipe" => OutputErrorMode::ExitNoPipe, + _ => unreachable!("clap excluded it"), + }) + .or_else(|| ignore_pipe_errors.then_some(OutputErrorMode::WarnNoPipe)); let files = matches .get_many::(options::FILE) @@ -142,6 +134,7 @@ pub fn uu_app() -> Command { .long(options::OUTPUT_ERROR) .require_equals(true) .num_args(0..=1) + .default_missing_value("warn-nopipe") .value_parser(ShortcutValueParser::new([ PossibleValue::new("warn").help(translate!("tee-help-output-error-warn")), PossibleValue::new("warn-nopipe") From b75872eb21d04d53e3e56aac8cbac03d4a64e05e Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Thu, 2 Apr 2026 14:50:32 +0900 Subject: [PATCH 131/264] tty: replace nix by rustix --- Cargo.lock | 2 +- src/uu/tty/Cargo.toml | 2 +- src/uu/tty/src/tty.rs | 6 ++++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c0b7cb3b3b1..2077096f441 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4349,7 +4349,7 @@ version = "0.8.0" dependencies = [ "clap", "fluent", - "nix", + "rustix", "uucore", ] diff --git a/src/uu/tty/Cargo.toml b/src/uu/tty/Cargo.toml index fe5a97b07e7..6fe03e42525 100644 --- a/src/uu/tty/Cargo.toml +++ b/src/uu/tty/Cargo.toml @@ -24,7 +24,7 @@ uucore = { workspace = true, features = ["fs"] } fluent = { workspace = true } [target.'cfg(unix)'.dependencies] -nix = { workspace = true, features = ["term"] } +rustix = { workspace = true, features = ["fs", "termios"] } [[bin]] name = "tty" diff --git a/src/uu/tty/src/tty.rs b/src/uu/tty/src/tty.rs index cb5ae8e1721..3dfef73225e 100644 --- a/src/uu/tty/src/tty.rs +++ b/src/uu/tty/src/tty.rs @@ -39,10 +39,12 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let mut stdout = std::io::stdout(); - let name = nix::unistd::ttyname(std::io::stdin()); + let name = rustix::termios::ttyname(std::io::stdin(), Vec::with_capacity(8)); let write_result = if let Ok(name) = name { - stdout.write_all_os(name.as_os_str()) + use std::os::unix::ffi::OsStrExt; + let os_name = std::ffi::OsStr::from_bytes(name.as_bytes()); + stdout.write_all_os(os_name) } else { set_exit_code(1); writeln!(stdout, "{}", translate!("tty-not-a-tty")) From 422f0145b2ea59265cd649d0bb498e3474007a93 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Fri, 27 Mar 2026 13:19:27 +0900 Subject: [PATCH 132/264] cat: improve splice fast-path --- .vscode/cspell.dictionaries/workspace.wordlist.txt | 1 + src/uu/cat/src/splice.rs | 11 ++++++++--- src/uucore/src/lib/features/pipes.rs | 7 ++++++- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/.vscode/cspell.dictionaries/workspace.wordlist.txt b/.vscode/cspell.dictionaries/workspace.wordlist.txt index 72f2d95f11e..a32ca0770ed 100644 --- a/.vscode/cspell.dictionaries/workspace.wordlist.txt +++ b/.vscode/cspell.dictionaries/workspace.wordlist.txt @@ -374,6 +374,7 @@ weblate algs wasm wasip +SETPIPE # * stty terminal flags brkint diff --git a/src/uu/cat/src/splice.rs b/src/uu/cat/src/splice.rs index a8b01e22605..268f0296c71 100644 --- a/src/uu/cat/src/splice.rs +++ b/src/uu/cat/src/splice.rs @@ -7,9 +7,8 @@ use super::{CatResult, FdReadable, InputHandle}; use rustix::io::{read, write}; use std::os::{fd::AsFd, unix::io::AsRawFd}; -use uucore::pipes::{pipe, splice, splice_exact}; +use uucore::pipes::{MAX_ROOTLESS_PIPE_SIZE, pipe, splice, splice_exact}; -const SPLICE_SIZE: usize = 1024 * 128; const BUF_SIZE: usize = 1024 * 16; /// This function is called from `write_fast()` on Linux and Android. The @@ -24,10 +23,16 @@ pub(super) fn write_fast_using_splice( handle: &InputHandle, write_fd: &S, ) -> CatResult { + use nix::fcntl::{FcntlArg, fcntl}; let (pipe_rd, pipe_wr) = pipe()?; + // improve performance + let _ = fcntl( + write_fd, + FcntlArg::F_SETPIPE_SZ(MAX_ROOTLESS_PIPE_SIZE as i32), + ); loop { - match splice(&handle.reader, &pipe_wr, SPLICE_SIZE) { + match splice(&handle.reader, &pipe_wr, MAX_ROOTLESS_PIPE_SIZE) { Ok(n) => { if n == 0 { return Ok(false); diff --git a/src/uucore/src/lib/features/pipes.rs b/src/uucore/src/lib/features/pipes.rs index 776cf2711aa..6f613558e65 100644 --- a/src/uucore/src/lib/features/pipes.rs +++ b/src/uucore/src/lib/features/pipes.rs @@ -4,21 +4,26 @@ // file that was distributed with this source code. //! Thin zero-copy-related wrappers around functions from the `rustix` crate. + #[cfg(any(target_os = "linux", target_os = "android"))] use std::fs::File; #[cfg(any(target_os = "linux", target_os = "android"))] use std::os::fd::AsFd; - #[cfg(any(target_os = "linux", target_os = "android"))] use rustix::pipe::SpliceFlags; +pub const MAX_ROOTLESS_PIPE_SIZE: usize = 1024 * 1024; /// A wrapper around [`rustix::pipe::pipe`] that ensures the pipe is cleaned up. /// /// Returns two `File` objects: everything written to the second can be read /// from the first. +/// This is used only for resolving the limitation for splice: one of a input or output should be pipe #[cfg(any(target_os = "linux", target_os = "android"))] pub fn pipe() -> std::io::Result<(File, File)> { let (read, write) = rustix::pipe::pipe()?; + // improve performance for splice + let _ = fcntl(&read, FcntlArg::F_SETPIPE_SZ(MAX_ROOTLESS_PIPE_SIZE as i32)); + Ok((File::from(read), File::from(write))) } From 68a05d9d1eb628de04cfa08330046549fab60859 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Tue, 31 Mar 2026 11:21:43 +0900 Subject: [PATCH 133/264] cat: avoid pipe() if stdout is pipe --- .../workspace.wordlist.txt | 2 +- src/uu/cat/src/splice.rs | 53 +++++++++++-------- src/uucore/src/lib/features/pipes.rs | 6 +-- 3 files changed, 34 insertions(+), 27 deletions(-) diff --git a/.vscode/cspell.dictionaries/workspace.wordlist.txt b/.vscode/cspell.dictionaries/workspace.wordlist.txt index a32ca0770ed..04a1402158b 100644 --- a/.vscode/cspell.dictionaries/workspace.wordlist.txt +++ b/.vscode/cspell.dictionaries/workspace.wordlist.txt @@ -367,6 +367,7 @@ uutils # * function names getcwd +setpipe # * other getlimits @@ -374,7 +375,6 @@ weblate algs wasm wasip -SETPIPE # * stty terminal flags brkint diff --git a/src/uu/cat/src/splice.rs b/src/uu/cat/src/splice.rs index 268f0296c71..14e88dd6e44 100644 --- a/src/uu/cat/src/splice.rs +++ b/src/uu/cat/src/splice.rs @@ -23,33 +23,40 @@ pub(super) fn write_fast_using_splice( handle: &InputHandle, write_fd: &S, ) -> CatResult { - use nix::fcntl::{FcntlArg, fcntl}; - let (pipe_rd, pipe_wr) = pipe()?; - // improve performance - let _ = fcntl( - write_fd, - FcntlArg::F_SETPIPE_SZ(MAX_ROOTLESS_PIPE_SIZE as i32), - ); - - loop { - match splice(&handle.reader, &pipe_wr, MAX_ROOTLESS_PIPE_SIZE) { - Ok(n) => { - if n == 0 { - return Ok(false); + const FIRST_PIPE_SIZE: usize = 64 * 1024; + if splice(&handle.reader, &write_fd, FIRST_PIPE_SIZE).is_ok() { + // fcntl improves performance for large file which is large overhead for small files + let _ = rustix::pipe::fcntl_setpipe_size(write_fd, MAX_ROOTLESS_PIPE_SIZE); + loop { + match splice(&handle.reader, &write_fd, MAX_ROOTLESS_PIPE_SIZE) { + Ok(1..) => {} + Ok(0) => return Ok(false), + Err(_) => return Ok(true), + } + } + } else { + // output is not pipe. Needs broker to use splice() which is high cost for small files + let (pipe_rd, pipe_wr) = pipe()?; + loop { + match splice(&handle.reader, &pipe_wr, MAX_ROOTLESS_PIPE_SIZE) { + Ok(n) => { + if n == 0 { + return Ok(false); + } + if splice_exact(&pipe_rd, write_fd, n).is_err() { + // If the first splice manages to copy to the intermediate + // pipe, but the second splice to stdout fails for some reason + // we can recover by copying the data that we have from the + // intermediate pipe to stdout using normal read/write. Then + // we tell the caller to fall back. + copy_exact(&pipe_rd, write_fd, n)?; + return Ok(true); + } } - if splice_exact(&pipe_rd, write_fd, n).is_err() { - // If the first splice manages to copy to the intermediate - // pipe, but the second splice to stdout fails for some reason - // we can recover by copying the data that we have from the - // intermediate pipe to stdout using normal read/write. Then - // we tell the caller to fall back. - copy_exact(&pipe_rd, write_fd, n)?; + Err(_) => { return Ok(true); } } - Err(_) => { - return Ok(true); - } } } } diff --git a/src/uucore/src/lib/features/pipes.rs b/src/uucore/src/lib/features/pipes.rs index 6f613558e65..2615912c08d 100644 --- a/src/uucore/src/lib/features/pipes.rs +++ b/src/uucore/src/lib/features/pipes.rs @@ -5,12 +5,12 @@ //! Thin zero-copy-related wrappers around functions from the `rustix` crate. +#[cfg(any(target_os = "linux", target_os = "android"))] +use rustix::pipe::{SpliceFlags, fcntl_setpipe_size}; #[cfg(any(target_os = "linux", target_os = "android"))] use std::fs::File; #[cfg(any(target_os = "linux", target_os = "android"))] use std::os::fd::AsFd; -#[cfg(any(target_os = "linux", target_os = "android"))] -use rustix::pipe::SpliceFlags; pub const MAX_ROOTLESS_PIPE_SIZE: usize = 1024 * 1024; /// A wrapper around [`rustix::pipe::pipe`] that ensures the pipe is cleaned up. @@ -22,7 +22,7 @@ pub const MAX_ROOTLESS_PIPE_SIZE: usize = 1024 * 1024; pub fn pipe() -> std::io::Result<(File, File)> { let (read, write) = rustix::pipe::pipe()?; // improve performance for splice - let _ = fcntl(&read, FcntlArg::F_SETPIPE_SZ(MAX_ROOTLESS_PIPE_SIZE as i32)); + let _ = fcntl_setpipe_size(&read, MAX_ROOTLESS_PIPE_SIZE); Ok((File::from(read), File::from(write))) } From 1955f6197a7bdafc088d02f924d8e232ffa4a47a Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Thu, 2 Apr 2026 13:55:52 +0900 Subject: [PATCH 134/264] cat: add bench for large size for splice --- src/uu/cat/benches/cat_bench.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uu/cat/benches/cat_bench.rs b/src/uu/cat/benches/cat_bench.rs index b589eb55ed8..2e351970f78 100644 --- a/src/uu/cat/benches/cat_bench.rs +++ b/src/uu/cat/benches/cat_bench.rs @@ -2,7 +2,7 @@ use divan::{Bencher, black_box}; use uu_cat::uumain; use uucore::benchmark::{run_util_function, setup_test_file}; -#[divan::bench(args = [10_000])] +#[divan::bench(args = [10_000, 10_000_000])] fn cat_default(bencher: Bencher, size_bytes: usize) { let data = vec![b'a'; size_bytes]; From 7214dbfd41a68022804181ba9345bc6e82dc2214 Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Thu, 2 Apr 2026 07:52:55 +0200 Subject: [PATCH 135/264] Bump icu_* crates from 2.1.1 to 2.2.0 --- Cargo.lock | 91 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 47 insertions(+), 44 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2077096f441..1a9269ddcb0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -283,9 +283,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "calendrical_calculations" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a0b39595c6ee54a8d0900204ba4c401d0ab4eb45adaf07178e8d017541529e7" +checksum = "5abbd6eeda6885048d357edc66748eea6e0268e3dd11f326fff5bd248d779c26" dependencies = [ "core_maths", "displaydoc", @@ -1084,9 +1084,9 @@ checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "fixed_decimal" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35eabf480f94d69182677e37571d3be065822acfafd12f2f085db44fbbcc8e57" +checksum = "79c3c892f121fff406e5dd6b28c1b30096b95111c30701a899d4f2b18da6d1bd" dependencies = [ "displaydoc", "smallvec", @@ -1382,9 +1382,9 @@ dependencies = [ [[package]] name = "icu_calendar" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f0e52e009b6b16ba9c0693578796f2dd4aaa59a7f8f920423706714a89ac4e" +checksum = "baf11bd83ebd0cd319e23a9a9c9a25b564a1fadc3bbc93069b8abc9d8df812bf" dependencies = [ "calendrical_calculations", "displaydoc", @@ -1399,15 +1399,15 @@ dependencies = [ [[package]] name = "icu_calendar_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527f04223b17edfe0bd43baf14a0cb1b017830db65f3950dc00224860a9a446d" +checksum = "118577bcf3a0fa7c6ac0a7d6e951814da84ee56b9b1f68fb4d8d10b08cefaf4d" [[package]] name = "icu_collator" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32eed11a5572f1088b63fa21dc2e70d4a865e5739fc2d10abc05be93bae97019" +checksum = "b521b92a2666061ddda902769d8a4cf730b5c9529a845cc1b69770b12a6c9a71" dependencies = [ "icu_collator_data", "icu_collections", @@ -1424,18 +1424,19 @@ dependencies = [ [[package]] name = "icu_collator_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ab06f0e83a613efddba3e4913e00e43ed4001fae651cb7d40fc7e66b83b6fb9" +checksum = "038ed8e5817f2059c2f3efb0945ba78d060d3d25e8f1a1bea5139f821a21a2f0" [[package]] name = "icu_collections" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -1443,9 +1444,9 @@ dependencies = [ [[package]] name = "icu_datetime" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9d49f41ded8e63761b6b4c3120dfdc289415a1ed10107db6198eb311057ca5" +checksum = "989d56ea5bbc43ae2b4e0388874b002884eaf4ed3a76c84a6c8c5ad575e04d72" dependencies = [ "displaydoc", "fixed_decimal", @@ -1466,20 +1467,22 @@ dependencies = [ [[package]] name = "icu_datetime_data" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46597233625417b7c8052a63d916e4fdc73df21614ac0b679492a5d6e3b01aeb" +checksum = "40d3cc1b690d9703202bc319692ac8a1f3a6390686f0930ff40542450fa34f0b" [[package]] name = "icu_decimal" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a38c52231bc348f9b982c1868a2af3195199623007ba2c7650f432038f5b3e8e" +checksum = "288247df2e32aa776ac54fdd64de552149ac43cb840f2761811f0e8d09719dd4" dependencies = [ + "displaydoc", "fixed_decimal", "icu_decimal_data", "icu_locale", "icu_locale_core", + "icu_plurals", "icu_provider", "writeable", "zerovec", @@ -1487,15 +1490,15 @@ dependencies = [ [[package]] name = "icu_decimal_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2905b4044eab2dd848fe84199f9195567b63ab3a93094711501363f63546fef7" +checksum = "6f14a5ca9e8af29eef62064f269078424283d90dbaffeac5225addf62aaabc22" [[package]] name = "icu_locale" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "532b11722e350ab6bf916ba6eb0efe3ee54b932666afec989465f9243fe6dd60" +checksum = "d5a396343c7208121dc86e35623d3dfe19814a7613cfd14964994cdc9c9a2e26" dependencies = [ "icu_collections", "icu_locale_core", @@ -1522,15 +1525,15 @@ dependencies = [ [[package]] name = "icu_locale_data" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c5f1d16b4c3a2642d3a719f18f6b06070ab0aef246a6418130c955ae08aa831" +checksum = "d5fdcc9ac77c6d74ff5cf6e65ef3181d6af32003b16fce3a77fb451d2f695993" [[package]] name = "icu_normalizer" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -1545,15 +1548,15 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_pattern" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a7ff8c0ff6f61cdce299dcb54f557b0a251adbc78f6f0c35a21332c452b4a1b" +checksum = "1c4c568054ffe735398a9f4c55aec37ad7c768844553cc0978f09cc9b933a1fb" dependencies = [ "displaydoc", "either", @@ -1564,9 +1567,9 @@ dependencies = [ [[package]] name = "icu_plurals" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f9cfe49f5b1d1163cc58db451562339916a9ca5cbcaae83924d41a0bf839474" +checksum = "2a50023f1d49ad5c4333380328a0d4a19e4b9d6d842ec06639affd5ba47c8103" dependencies = [ "fixed_decimal", "icu_locale", @@ -1577,15 +1580,15 @@ dependencies = [ [[package]] name = "icu_plurals_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f018a98dccf7f0eb02ba06ac0ff67d102d8ded80734724305e924de304e12ff0" +checksum = "8485497155dc865f901decb93ecc20d3e467df67bfeceb91e3ba34e2b11e8e1d" [[package]] name = "icu_properties" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ "icu_collections", "icu_locale_core", @@ -1597,9 +1600,9 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" @@ -1620,9 +1623,9 @@ dependencies = [ [[package]] name = "icu_time" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8242b00da3b3b6678f731437a11c8833a43c821ae081eca60ba1b7579d45b6d8" +checksum = "ec3af0c141da0a61d4f6970cd1d5f4b388b17ea22f8124f8f6049d3d5147586a" dependencies = [ "calendrical_calculations", "displaydoc", @@ -1637,9 +1640,9 @@ dependencies = [ [[package]] name = "icu_time_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e10b0e5e87a2c84bd5fa407705732052edebe69291d347d0c3033785470edbf" +checksum = "6f2f8aeca682d874a5247084aa4fb7d1cef9ba45d889c21209a8818dcaaa0ec9" [[package]] name = "id-arena" From 09422700d2bd4fd24d521607c7f3f49055b08517 Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Thu, 2 Apr 2026 09:37:36 +0200 Subject: [PATCH 136/264] uucore/datetime: adapt to change in icu_calendar --- src/uucore/src/lib/features/i18n/datetime.rs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/uucore/src/lib/features/i18n/datetime.rs b/src/uucore/src/lib/features/i18n/datetime.rs index c79b4426fa9..ce52013605f 100644 --- a/src/uucore/src/lib/features/i18n/datetime.rs +++ b/src/uucore/src/lib/features/i18n/datetime.rs @@ -82,15 +82,27 @@ pub fn localize_format_string(format: &str, date: JiffDate) -> String { let (cal_year, cal_month, cal_day) = match calendar_type { CalendarType::Buddhist => { let d = iso_date.to_calendar(Buddhist); - (d.extended_year(), d.month().ordinal, d.day_of_month().0) + ( + d.year().extended_year(), + d.month().ordinal, + d.day_of_month().0, + ) } CalendarType::Persian => { let d = iso_date.to_calendar(Persian); - (d.extended_year(), d.month().ordinal, d.day_of_month().0) + ( + d.year().extended_year(), + d.month().ordinal, + d.day_of_month().0, + ) } CalendarType::Ethiopian => { let d = iso_date.to_calendar(Ethiopian::new()); - (d.extended_year(), d.month().ordinal, d.day_of_month().0) + ( + d.year().extended_year(), + d.month().ordinal, + d.day_of_month().0, + ) } CalendarType::Gregorian => unreachable!(), }; From af91721116928aff834742115dcb1a986c59ee25 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Mon, 30 Mar 2026 20:37:13 +0200 Subject: [PATCH 137/264] feat_wasm: add head to the WASM-compatible utilities --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index 9cdfab875e7..efbf590e492 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -204,6 +204,7 @@ feat_wasm = [ "pathchk", "pr", "printenv", + "head", "printf", "ptx", "pwd", From e428236094d81da9e01b6667b5f9fdb187e7d690 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Mon, 30 Mar 2026 20:42:26 +0200 Subject: [PATCH 138/264] cat: add WASI stub for is_unsafe_overwrite and add to feat_wasm WASI has no fstat-based device/inode checks, so always return false (assume safe) for the overwrite detection. --- Cargo.toml | 1 + src/uu/cat/src/platform/mod.rs | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index efbf590e492..33fdbe0d25e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -184,6 +184,7 @@ feat_wasm = [ "base32", "base64", "basenc", + "cat", "cut", "ls", "date", diff --git a/src/uu/cat/src/platform/mod.rs b/src/uu/cat/src/platform/mod.rs index 3fa27a27686..80ef6e233ac 100644 --- a/src/uu/cat/src/platform/mod.rs +++ b/src/uu/cat/src/platform/mod.rs @@ -9,6 +9,12 @@ pub use self::unix::is_unsafe_overwrite; #[cfg(windows)] pub use self::windows::is_unsafe_overwrite; +// WASI: no fstat-based device/inode checks available; assume safe. +#[cfg(target_os = "wasi")] +pub fn is_unsafe_overwrite(_input: &I, _output: &O) -> bool { + false +} + #[cfg(unix)] mod unix; From 8c5babb41896e75c15f6153596e518af449afdb6 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Mon, 30 Mar 2026 21:16:26 +0200 Subject: [PATCH 139/264] uucore: add WASI support for mode and path_ends_with_terminator - mode: return default umask (022) on WASI instead of calling libc::umask - fs: add WASI variant of path_ends_with_terminator using string conversion --- src/uucore/src/lib/features/fs.rs | 5 ++++- src/uucore/src/lib/features/mode.rs | 10 ++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/uucore/src/lib/features/fs.rs b/src/uucore/src/lib/features/fs.rs index bc7f92dd78b..ede2d07ee7e 100644 --- a/src/uucore/src/lib/features/fs.rs +++ b/src/uucore/src/lib/features/fs.rs @@ -763,9 +763,12 @@ pub fn are_hardlinks_or_one_way_symlink_to_same_file(source: &Path, target: &Pat /// # Arguments /// /// * `path` - A reference to the path to be checked. -#[cfg(unix)] +#[cfg(any(unix, target_os = "wasi"))] pub fn path_ends_with_terminator(path: &Path) -> bool { + #[cfg(unix)] use std::os::unix::prelude::OsStrExt; + #[cfg(target_os = "wasi")] + use std::os::wasi::ffi::OsStrExt; path.as_os_str() .as_bytes() .last() diff --git a/src/uucore/src/lib/features/mode.rs b/src/uucore/src/lib/features/mode.rs index 4e737595ee2..6a96307b2b7 100644 --- a/src/uucore/src/lib/features/mode.rs +++ b/src/uucore/src/lib/features/mode.rs @@ -7,7 +7,7 @@ // spell-checker:ignore (vars) fperm srwx -#[cfg(not(unix))] +#[cfg(windows)] use libc::umask; pub fn parse_numeric(fperm: u32, mut mode: &str, considering_dir: bool) -> Result { @@ -185,7 +185,7 @@ pub fn get_umask() -> u32 { mask.bits() as u32 } - #[cfg(not(unix))] + #[cfg(windows)] { // SAFETY: umask always succeeds and doesn't operate on memory. Races are // possible but it can't violate Rust's guarantees. @@ -193,6 +193,12 @@ pub fn get_umask() -> u32 { unsafe { umask(mask) }; mask as u32 } + + // WASI has no umask; return a typical default (022). + #[cfg(not(any(unix, windows)))] + { + 0o022 + } } #[cfg(test)] From f0e45eef6ebd4f337f0e1db1f6e268ecfda09655 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Mon, 30 Mar 2026 21:16:32 +0200 Subject: [PATCH 140/264] cp, mv: return error for symlinks on non-unix/non-windows platforms Replace cfg(not(windows)) with cfg(unix) for the symlink_file call, and add a fallback that returns an unsupported error on platforms like WASI that lack symlink support. --- src/uu/cp/src/cp.rs | 12 +++++++++++- src/uu/mv/src/mv.rs | 5 +++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index e781cc14ba4..af1243779bf 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -1898,7 +1898,17 @@ fn symlink_file( dest: &Path, symlinked_files: &mut HashSet, ) -> CopyResult<()> { - #[cfg(not(windows))] + #[cfg(target_os = "wasi")] + { + return Err(CpError::IoErrContext( + std::io::Error::new(std::io::ErrorKind::Unsupported, "symlinks not supported"), + translate!("cp-error-cannot-create-symlink", + "dest" => get_filename(dest).unwrap_or("?").quote(), + "source" => get_filename(source).unwrap_or("?").quote()), + ) + .into()); + } + #[cfg(not(any(windows, target_os = "wasi")))] { std::os::unix::fs::symlink(source, dest).map_err(|e| { CpError::IoErrContext( diff --git a/src/uu/mv/src/mv.rs b/src/uu/mv/src/mv.rs index 2f69801e15a..fa106ac82c0 100644 --- a/src/uu/mv/src/mv.rs +++ b/src/uu/mv/src/mv.rs @@ -821,6 +821,8 @@ fn rename_with_fallback( const EXDEV: i32 = windows_sys::Win32::Foundation::ERROR_NOT_SAME_DEVICE as _; #[cfg(unix)] const EXDEV: i32 = libc::EXDEV as _; + #[cfg(target_os = "wasi")] + const EXDEV: i32 = 18; // POSIX EXDEV value // We will only copy if: // 1. Files are on different devices (EXDEV error) @@ -926,9 +928,8 @@ fn rename_symlink_fallback(from: &Path, to: &Path) -> io::Result<()> { } } -#[cfg(not(any(windows, unix)))] +#[cfg(target_os = "wasi")] fn rename_symlink_fallback(from: &Path, to: &Path) -> io::Result<()> { - let path_symlink_points_to = fs::read_link(from)?; Err(io::Error::new( io::ErrorKind::Other, translate!("mv-error-no-symlink-support"), From 84bb51f9d91717145ffe23e8911668052c5d5cc0 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Tue, 31 Mar 2026 22:50:40 +0200 Subject: [PATCH 141/264] feat_wasm: add tr, cp, and mv to the WASM-compatible utilities --- Cargo.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 33fdbe0d25e..0d38e475ead 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -185,6 +185,7 @@ feat_wasm = [ "base64", "basenc", "cat", + "cp", "cut", "ls", "date", @@ -198,6 +199,7 @@ feat_wasm = [ "fold", "join", "link", + "mv", "nl", "numfmt", "od", @@ -215,6 +217,7 @@ feat_wasm = [ "sleep", "sum", "tee", + "tr", "true", "truncate", "uname", From 5b71c57ef6be5d8618da54b8976b8a744dca9f4d Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Thu, 2 Apr 2026 11:09:05 +0200 Subject: [PATCH 142/264] sort: disable rayon on wasi --- src/uu/sort/src/sort.rs | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/uu/sort/src/sort.rs b/src/uu/sort/src/sort.rs index 203bb30a111..7057ec564ed 100644 --- a/src/uu/sort/src/sort.rs +++ b/src/uu/sort/src/sort.rs @@ -29,6 +29,7 @@ use foldhash::fast::FoldHasher; use foldhash::{HashMap, SharedSeed}; use numeric_str_cmp::{NumInfo, NumInfoParseSettings, human_numeric_str_cmp, numeric_str_cmp}; use rand::{RngExt as _, rng}; +#[cfg(not(target_os = "wasi"))] use rayon::prelude::*; use std::cmp::Ordering; use std::env; @@ -2127,9 +2128,12 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { Ok(0) | Err(_) => std::thread::available_parallelism().map_or(1, NonZero::get), Ok(n) => n, }; - let _ = rayon::ThreadPoolBuilder::new() - .num_threads(num_threads) - .build_global(); + #[cfg(not(target_os = "wasi"))] + { + let _ = rayon::ThreadPoolBuilder::new() + .num_threads(num_threads) + .build_global(); + } } if let Some(size_str) = matches.get_one::(options::BUF_SIZE) { @@ -2602,10 +2606,19 @@ fn exec( } fn sort_by<'a>(unsorted: &mut Vec>, settings: &GlobalSettings, line_data: &LineData<'a>) { + let cmp = |a: &Line<'a>, b: &Line<'a>| compare_by(a, b, settings, line_data, line_data); + // WASI does not support threads, so use non-parallel sort to avoid + // rayon's thread pool which triggers an unreachable trap. if settings.stable || settings.unique { - unsorted.par_sort_by(|a, b| compare_by(a, b, settings, line_data, line_data)); + #[cfg(not(target_os = "wasi"))] + unsorted.par_sort_by(cmp); + #[cfg(target_os = "wasi")] + unsorted.sort_by(cmp); } else { - unsorted.par_sort_unstable_by(|a, b| compare_by(a, b, settings, line_data, line_data)); + #[cfg(not(target_os = "wasi"))] + unsorted.par_sort_unstable_by(cmp); + #[cfg(target_os = "wasi")] + unsorted.sort_unstable_by(cmp); } } From f1bfe37cef77af75e1dcf47766a4436a54b86584 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Thu, 2 Apr 2026 11:10:43 +0200 Subject: [PATCH 143/264] sort: replace wildcard rayon import with specific import --- src/uu/sort/src/sort.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uu/sort/src/sort.rs b/src/uu/sort/src/sort.rs index 7057ec564ed..c3ba3dcb27a 100644 --- a/src/uu/sort/src/sort.rs +++ b/src/uu/sort/src/sort.rs @@ -30,7 +30,7 @@ use foldhash::{HashMap, SharedSeed}; use numeric_str_cmp::{NumInfo, NumInfoParseSettings, human_numeric_str_cmp, numeric_str_cmp}; use rand::{RngExt as _, rng}; #[cfg(not(target_os = "wasi"))] -use rayon::prelude::*; +use rayon::slice::ParallelSliceMut; use std::cmp::Ordering; use std::env; use std::ffi::{OsStr, OsString}; From 07eed6db18ce508abff547da6753852d4f73ede5 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Thu, 2 Apr 2026 20:25:55 +0900 Subject: [PATCH 144/264] tsort: replace nix by rustix --- Cargo.lock | 2 +- src/uu/tsort/Cargo.toml | 2 +- src/uu/tsort/src/tsort.rs | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1a9269ddcb0..ce852b571cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4339,8 +4339,8 @@ dependencies = [ "clap", "codspeed-divan-compat", "fluent", - "nix", "rustc-hash", + "rustix", "string-interner", "thiserror 2.0.18", "uucore", diff --git a/src/uu/tsort/Cargo.toml b/src/uu/tsort/Cargo.toml index 84a31349643..227cab634ea 100644 --- a/src/uu/tsort/Cargo.toml +++ b/src/uu/tsort/Cargo.toml @@ -28,7 +28,7 @@ uucore = { workspace = true } rustc-hash = { workspace = true } [target.'cfg(unix)'.dependencies] -nix = { workspace = true, features = ["fs"] } +rustix = { workspace = true, features = ["fs"] } [[bin]] name = "tsort" diff --git a/src/uu/tsort/src/tsort.rs b/src/uu/tsort/src/tsort.rs index a689cd05be4..5b2fb66d6d5 100644 --- a/src/uu/tsort/src/tsort.rs +++ b/src/uu/tsort/src/tsort.rs @@ -87,14 +87,14 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { target_os = "freebsd", ))] { - use nix::fcntl::{PosixFadviseAdvice, posix_fadvise}; + use rustix::fs::{Advice, fadvise}; use std::os::unix::io::AsFd; - posix_fadvise( + fadvise( file.as_fd(), - 0, // offset 0 => from the start of the file - 0, // length 0 => for the whole file - PosixFadviseAdvice::POSIX_FADV_SEQUENTIAL, + 0, // offset 0 => from the start of the file + None, // None => for the whole file + Advice::Sequential, ) .ok(); } From 62f26a886b1abb157864f642095052fa87a58592 Mon Sep 17 00:00:00 2001 From: Michel Lind Date: Thu, 19 Mar 2026 15:19:55 +0000 Subject: [PATCH 145/264] uu_stdbuf_libstdbuf: add LICENSE Needed for distributing MIT licensed software ``` $ cargo package --allow-dirty --list | grep LICENSE LICENSE ``` Signed-off-by: Michel Lind --- src/uu/stdbuf/src/libstdbuf/LICENSE | 1 + 1 file changed, 1 insertion(+) create mode 120000 src/uu/stdbuf/src/libstdbuf/LICENSE diff --git a/src/uu/stdbuf/src/libstdbuf/LICENSE b/src/uu/stdbuf/src/libstdbuf/LICENSE new file mode 120000 index 00000000000..2a64f9d0fc6 --- /dev/null +++ b/src/uu/stdbuf/src/libstdbuf/LICENSE @@ -0,0 +1 @@ +../../../../../LICENSE \ No newline at end of file From 8d0361fe49301700a5dc8ead127292e6e069ec90 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 17:28:45 +0000 Subject: [PATCH 146/264] chore(deps): update rust crate icu_calendar to v2.2.1 --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ce852b571cb..244332276c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1382,9 +1382,9 @@ dependencies = [ [[package]] name = "icu_calendar" -version = "2.2.0" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf11bd83ebd0cd319e23a9a9c9a25b564a1fadc3bbc93069b8abc9d8df812bf" +checksum = "a2b2acc6263f494f1df50685b53ff8e57869e47d5c6fe39c23d518ae9a4f3e45" dependencies = [ "calendrical_calculations", "displaydoc", From a53ba55325e4b5407a1dcb944a90a079c9b997ac Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 3 Apr 2026 05:19:58 +0000 Subject: [PATCH 147/264] chore(deps): update dawidd6/action-download-artifact action to v20 --- .github/workflows/GnuTests.yml | 2 +- .github/workflows/make.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/GnuTests.yml b/.github/workflows/GnuTests.yml index e350c31bcf3..e063c70552a 100644 --- a/.github/workflows/GnuTests.yml +++ b/.github/workflows/GnuTests.yml @@ -354,7 +354,7 @@ jobs: path: 'uutils' persist-credentials: false - name: Retrieve reference artifacts - uses: dawidd6/action-download-artifact@v19 + uses: dawidd6/action-download-artifact@v20 # ref: continue-on-error: true ## don't break the build for missing reference artifacts (may be expired or just not generated yet) with: diff --git a/.github/workflows/make.yml b/.github/workflows/make.yml index 5ac2d13f347..1ebe630c124 100644 --- a/.github/workflows/make.yml +++ b/.github/workflows/make.yml @@ -201,14 +201,14 @@ jobs: --arg multisize "$SIZE_MULTI" \ '{($date): { sha: $sha, size: $size, multisize: $multisize, }}' > size-result.json - name: Download the previous individual size result - uses: dawidd6/action-download-artifact@v19 + uses: dawidd6/action-download-artifact@v20 with: workflow: make.yml name: individual-size-result repo: uutils/coreutils path: dl - name: Download the previous size result - uses: dawidd6/action-download-artifact@v19 + uses: dawidd6/action-download-artifact@v20 with: workflow: make.yml name: size-result From 480e5fb2f01aef7b6c64b8e791b2abf892973330 Mon Sep 17 00:00:00 2001 From: xtqqczze <45661989+xtqqczze@users.noreply.github.com> Date: Fri, 3 Apr 2026 00:54:53 +0100 Subject: [PATCH 148/264] pre-commit: update rust-clippy and cargo-lock-check to run without accessing the network --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9abeb5ca476..198ccb2087a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -40,14 +40,14 @@ repos: - id: rust-clippy name: Rust clippy description: Run cargo clippy on files included in the commit. - entry: cargo +stable clippy --workspace --all-targets --all-features -- -D warnings + entry: cargo +stable clippy --frozen --workspace --all-targets --all-features -- -D warnings pass_filenames: false types: [file, rust] language: system - id: cargo-lock-check name: Cargo.lock sync check description: Ensure Cargo.lock and fuzz/Cargo.lock are up-to-date. - entry: bash -c 'for dir in . fuzz; do ( cd "$dir" && cargo fetch --locked --quiet ) || { echo "ERROR - $dir/Cargo.lock is out of date. Run -> cd $dir && cargo update"; exit 1; } done' + entry: bash -c 'for dir in . fuzz; do ( cd "$dir" && cargo fetch --frozen --quiet ) || { echo "ERROR - $dir/Cargo.lock is out of date. Run -> cd $dir && cargo update"; exit 1; } done' pass_filenames: false files: 'Cargo\.(toml|lock)$' language: system From 24f8aa67e225720dda0794839e7276983e389a31 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 3 Apr 2026 13:13:03 +0900 Subject: [PATCH 149/264] tail: replace nix by rustix --- Cargo.lock | 2 +- src/uu/tail/Cargo.toml | 2 +- src/uu/tail/src/tail.rs | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 244332276c7..84b43ce6320 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4246,9 +4246,9 @@ dependencies = [ "fluent", "libc", "memchr", - "nix", "notify", "rstest", + "rustix", "same-file", "uucore", "windows-sys 0.61.2", diff --git a/src/uu/tail/Cargo.toml b/src/uu/tail/Cargo.toml index 5e11a54119c..f67062d6e2b 100644 --- a/src/uu/tail/Cargo.toml +++ b/src/uu/tail/Cargo.toml @@ -29,7 +29,7 @@ same-file = { workspace = true } fluent = { workspace = true } [target.'cfg(unix)'.dependencies] -nix = { workspace = true, features = ["fs"] } +rustix = { workspace = true, features = ["fs"] } [target.'cfg(windows)'.dependencies] windows-sys = { workspace = true, features = [ diff --git a/src/uu/tail/src/tail.rs b/src/uu/tail/src/tail.rs index 5b0c6d75c36..6bd812a1eb9 100644 --- a/src/uu/tail/src/tail.rs +++ b/src/uu/tail/src/tail.rs @@ -220,7 +220,7 @@ fn tail_file( /// Without `--pid`, FIFOs block on open() until a writer connects (GNU behavior). #[cfg(unix)] fn open_file(path: &Path, use_nonblock_for_fifo: bool) -> io::Result { - use nix::fcntl::{FcntlArg, OFlag, fcntl}; + use rustix::fs::{OFlags, fcntl_getfl, fcntl_setfl}; use std::fs::OpenOptions; use std::os::fd::AsFd; use std::os::unix::fs::{FileTypeExt, OpenOptionsExt}; @@ -237,9 +237,9 @@ fn open_file(path: &Path, use_nonblock_for_fifo: bool) -> io::Result { .open(path)?; // Clear O_NONBLOCK so reads block normally - let flags = fcntl(file.as_fd(), FcntlArg::F_GETFL)?; - let new_flags = OFlag::from_bits_truncate(flags) & !OFlag::O_NONBLOCK; - fcntl(file.as_fd(), FcntlArg::F_SETFL(new_flags))?; + let flags = fcntl_getfl(file.as_fd())?; + let new_flags = flags & !OFlags::NONBLOCK; + fcntl_setfl(file.as_fd(), new_flags)?; Ok(file) } else { From b82527ed148a549451f228baabe3a732baf87d14 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Thu, 26 Mar 2026 20:56:52 +0900 Subject: [PATCH 150/264] sync: fix exit code when fcntl failed & 2>/dev/full abort --- src/uu/sync/src/sync.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/uu/sync/src/sync.rs b/src/uu/sync/src/sync.rs index b91f216aaed..738e305907c 100644 --- a/src/uu/sync/src/sync.rs +++ b/src/uu/sync/src/sync.rs @@ -68,10 +68,13 @@ mod platform { // Reset O_NONBLOCK flag if it was set (matches GNU behavior) // This is non-critical, so we log errors but don't fail if let Err(e) = fcntl(&f, FcntlArg::F_SETFL(OFlag::empty())) { - eprintln!( + use std::io::{Write, stderr}; + let _ = writeln!( + stderr(), "sync: {}", translate!("sync-warning-fcntl-failed", "file" => path, "error" => e.to_string()) ); + uucore::error::set_exit_code(1); } Ok(f) } From 5cdabb0c3bf7ed5d80586ffcd39c4397a348ae0f Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Fri, 3 Apr 2026 16:15:00 +0900 Subject: [PATCH 151/264] wc: improve wc -c perf - 1.67X faster (#11591) --- src/uu/wc/src/count_fast.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/uu/wc/src/count_fast.rs b/src/uu/wc/src/count_fast.rs index 9fe6f8145bc..247ef7d297f 100644 --- a/src/uu/wc/src/count_fast.rs +++ b/src/uu/wc/src/count_fast.rs @@ -29,11 +29,9 @@ const FILE_ATTRIBUTE_NORMAL: u32 = 128; #[cfg(any(target_os = "linux", target_os = "android"))] use libc::S_IFIFO; #[cfg(any(target_os = "linux", target_os = "android"))] -use uucore::pipes::{pipe, splice, splice_exact}; +use uucore::pipes::{MAX_ROOTLESS_PIPE_SIZE, pipe, splice, splice_exact}; const BUF_SIZE: usize = 256 * 1024; -#[cfg(any(target_os = "linux", target_os = "android"))] -const SPLICE_SIZE: usize = 128 * 1024; /// This is a Linux-specific function to count the number of bytes using the /// `splice` system call, which is faster than using `read`. @@ -55,11 +53,14 @@ fn count_bytes_using_splice(fd: &impl AsFd) -> Result { // Bit of an edge case, but it has been known to happen return Err(0); } + // todo: avoid generating broker if input is pipe (fcntl_setpipe_size succeed) and directly splice() to /dev/null to save RAM usage let (pipe_rd, pipe_wr) = pipe().map_err(|_| 0_usize)?; let mut byte_count = 0; + // improve throughput from pipe + let _ = rustix::pipe::fcntl_setpipe_size(fd, MAX_ROOTLESS_PIPE_SIZE); loop { - match splice(fd, &pipe_wr, SPLICE_SIZE) { + match splice(fd, &pipe_wr, MAX_ROOTLESS_PIPE_SIZE) { Ok(0) => break, Ok(res) => { byte_count += res; From 89a2dda71228e89c6bea4d80fa62a8229fcbf8c7 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Fri, 3 Apr 2026 16:15:37 +0900 Subject: [PATCH 152/264] dd: avoid 0-filling buf at read_and_discard (#11583) --- src/uu/dd/src/dd.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/uu/dd/src/dd.rs b/src/uu/dd/src/dd.rs index 6c7ce253d42..bd232a6a0cd 100644 --- a/src/uu/dd/src/dd.rs +++ b/src/uu/dd/src/dd.rs @@ -184,13 +184,14 @@ impl Num { /// directly in `buf_size`-sized chunks, matching GNU dd's behavior. /// Returns the total number of bytes actually read. fn read_and_discard(reader: &mut R, n: u64, buf_size: usize) -> io::Result { - let mut buf = vec![0u8; buf_size]; + // todo: consider splice()ing to /dev/null on Linux + let mut buf = Vec::with_capacity(buf_size); let mut total = 0u64; let mut remaining = n; - while remaining > 0 { - let to_read = cmp::min(remaining, buf_size as u64) as usize; - match reader.read(&mut buf[..to_read]) { + let to_read = cmp::min(remaining, buf_size as u64); + buf.clear(); + match reader.by_ref().take(to_read).read_to_end(&mut buf) { Ok(0) => break, // EOF Ok(bytes_read) => { total += bytes_read as u64; From 52965b37d6ecf7d15daf78742a53d09d76f5b478 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Can=20B=C3=B6l=C3=BCk?= Date: Fri, 3 Apr 2026 11:06:43 +0200 Subject: [PATCH 153/264] timeout: return timed-out status after kill grace period (#11392) Co-authored-by: Sylvestre Ledru --- src/uu/timeout/src/timeout.rs | 2 +- tests/by-util/test_timeout.rs | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/uu/timeout/src/timeout.rs b/src/uu/timeout/src/timeout.rs index cb483db8fb8..5d5511cf1ed 100644 --- a/src/uu/timeout/src/timeout.rs +++ b/src/uu/timeout/src/timeout.rs @@ -296,7 +296,7 @@ fn wait_or_kill_process( }); Ok(exit_code) } else { - Ok(ExitStatus::TimeoutFailed.into()) + Ok(ExitStatus::CommandTimedOut.into()) } } Ok(None) => { diff --git a/tests/by-util/test_timeout.rs b/tests/by-util/test_timeout.rs index bb2ea78b825..d6b0afb11d8 100644 --- a/tests/by-util/test_timeout.rs +++ b/tests/by-util/test_timeout.rs @@ -104,6 +104,13 @@ fn test_preserve_status() { } } +#[test] +fn test_kill_after_preserves_timeout_exit_without_preserve_status() { + new_ucmd!() + .args(&["-k", "1", "1", "sleep", "10"]) + .fails_with_code(124) + .no_output(); +} #[test] fn test_preserve_status_even_when_send_signal() { // When sending CONT signal, process doesn't get killed or stopped. From 4319b63221555ef31066ef84a8d01142073ffe97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Can=20B=C3=B6l=C3=BCk?= Date: Fri, 3 Apr 2026 11:07:58 +0200 Subject: [PATCH 154/264] sync: fall back to full sync when --file-system has no operands (#11393) Co-authored-by: Sylvestre Ledru --- src/uu/sync/src/sync.rs | 8 ++++++-- tests/by-util/test_sync.rs | 5 +++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/uu/sync/src/sync.rs b/src/uu/sync/src/sync.rs index 738e305907c..8e81fd03015 100644 --- a/src/uu/sync/src/sync.rs +++ b/src/uu/sync/src/sync.rs @@ -263,8 +263,12 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { #[allow(clippy::if_same_then_else)] if matches.get_flag(options::FILE_SYSTEM) { - #[cfg(any(target_os = "linux", target_os = "android", target_os = "windows"))] - syncfs(files)?; + if files.is_empty() { + sync()?; + } else { + #[cfg(any(target_os = "linux", target_os = "android", target_os = "windows"))] + syncfs(files)?; + } } else if matches.get_flag(options::DATA) { #[cfg(any(target_os = "linux", target_os = "android"))] fdatasync(files)?; diff --git a/tests/by-util/test_sync.rs b/tests/by-util/test_sync.rs index 8c09d86372a..fdb93b72fd4 100644 --- a/tests/by-util/test_sync.rs +++ b/tests/by-util/test_sync.rs @@ -31,6 +31,11 @@ fn test_sync_fs() { .succeeds(); } +#[test] +fn test_sync_fs_without_files_falls_back_to_full_sync() { + new_ucmd!().arg("--file-system").succeeds(); +} + #[test] fn test_sync_data() { // Todo add a second arg From 643405a893e4cdff7e08e9ae59bf024cc73429fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Can=20B=C3=B6l=C3=BCk?= Date: Fri, 3 Apr 2026 12:13:24 +0200 Subject: [PATCH 155/264] expr: fix eager evaluation of parenthesized dead branches (#11395) Co-authored-by: Sylvestre Ledru --- src/uu/expr/src/syntax_tree.rs | 23 ++--------------------- tests/by-util/test_expr.rs | 27 +++++++++++++++++++++++++-- 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/src/uu/expr/src/syntax_tree.rs b/src/uu/expr/src/syntax_tree.rs index a5c453c1745..22d0bde13d5 100644 --- a/src/uu/expr/src/syntax_tree.rs +++ b/src/uu/expr/src/syntax_tree.rs @@ -624,9 +624,6 @@ pub struct AstNode { #[derive(Debug, Clone)] #[cfg_attr(test, derive(Eq, PartialEq))] pub enum AstNodeInner { - Evaluated { - value: NumOrStr, - }, Leaf { value: MaybeNonUtf8String, }, @@ -650,15 +647,6 @@ impl AstNode { Parser::new(input).parse() } - pub fn evaluated(self) -> ExprResult { - Ok(Self { - id: get_next_id(), - inner: AstNodeInner::Evaluated { - value: self.eval()?, - }, - }) - } - pub fn eval(&self) -> ExprResult { // This function implements a recursive tree-walking algorithm, but uses an explicit // stack approach instead of native recursion to avoid potential stack overflow @@ -669,9 +657,6 @@ impl AstNode { while let Some(node) = stack.pop() { match &node.inner { - AstNodeInner::Evaluated { value, .. } => { - result_stack.insert(node.id, Ok(value.clone())); - } AstNodeInner::Leaf { value, .. } => { result_stack.insert(node.id, Ok(value.to_owned().into())); } @@ -896,9 +881,7 @@ impl<'a, S: AsRef> Parser<'a, S> { value: self.next()?.into(), }, b"(" => { - // Evaluate the node just after parsing to we detect arithmetic - // errors before checking for the closing parenthesis. - let s = self.parse_expression()?.evaluated()?; + let s = self.parse_expression()?; match self.next() { Ok(b")") => {} @@ -1070,9 +1053,7 @@ mod test { AstNode::parse(&["(", "1", "+", "2", ")", "*", "3"]), Ok(op( BinOp::Numeric(NumericOp::Mul), - op(BinOp::Numeric(NumericOp::Add), "1", "2") - .evaluated() - .unwrap(), + op(BinOp::Numeric(NumericOp::Add), "1", "2"), "3" )) ); diff --git a/tests/by-util/test_expr.rs b/tests/by-util/test_expr.rs index ec1cf91598f..c95465af7a7 100644 --- a/tests/by-util/test_expr.rs +++ b/tests/by-util/test_expr.rs @@ -217,6 +217,29 @@ fn test_and() { new_ucmd!().args(&["", "&", ""]).fails().stdout_only("0\n"); } +#[test] +fn test_parenthesized_short_circuit_dead_branches() { + new_ucmd!() + .args(&["1", "|", "(", "1", "/", "0", ")"]) + .succeeds() + .stdout_only("1\n"); + + new_ucmd!() + .args(&["0", "&", "(", "1", "/", "0", ")"]) + .fails_with_code(1) + .stdout_only("0\n"); + + new_ucmd!() + .args(&["1", "|", "(", "0", "&", "(", "1", "/", "0", ")", ")"]) + .succeeds() + .stdout_only("1\n"); + + new_ucmd!() + .args(&["0", "&", "(", "1", "|", "(", "1", "/", "0", ")", ")"]) + .fails_with_code(1) + .stdout_only("0\n"); +} + #[test] fn test_length_fail() { new_ucmd!().args(&["length", "αbcdef", "1"]).fails(); @@ -580,11 +603,11 @@ fn test_num_str_comparison() { } #[test] -fn test_eager_evaluation() { +fn test_missing_closing_parenthesis_reports_syntax_error() { new_ucmd!() .args(&["(", "1", "/", "0"]) .fails() - .stderr_contains("division by zero"); + .stderr_contains("expecting ')' after '0'"); } #[test] From ba2969fd47638841ff5402347b5acb51b97662c7 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 3 Apr 2026 12:14:59 +0200 Subject: [PATCH 156/264] tail: add WASI support by disabling notify and pid features (#11569) WASI has no file watching (inotify/kqueue) or process management APIs. Disable notify and libc on WASI, provide Observer stubs so tail compiles, and add fallback values for BACKEND and polling_help. The follow mode (-f) returns an error on WASI since it cannot work without a file watching backend. --- Cargo.toml | 1 + src/uu/tail/Cargo.toml | 6 ++-- src/uu/tail/src/args.rs | 2 ++ src/uu/tail/src/follow/mod.rs | 63 +++++++++++++++++++++++++++++++++ src/uu/tail/src/paths.rs | 4 +++ src/uu/tail/src/platform/mod.rs | 22 ++++++++++++ src/uu/tail/src/text.rs | 2 ++ 7 files changed, 98 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0d38e475ead..f606eae28fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -216,6 +216,7 @@ feat_wasm = [ "shuf", "sleep", "sum", + "tail", "tee", "tr", "true", diff --git a/src/uu/tail/Cargo.toml b/src/uu/tail/Cargo.toml index f67062d6e2b..eaf269f1097 100644 --- a/src/uu/tail/Cargo.toml +++ b/src/uu/tail/Cargo.toml @@ -21,13 +21,15 @@ path = "src/tail.rs" [dependencies] clap = { workspace = true } -libc = { workspace = true } memchr = { workspace = true } -notify = { workspace = true } uucore = { workspace = true, features = ["fs", "parser-size", "signals"] } same-file = { workspace = true } fluent = { workspace = true } +[target.'cfg(not(target_os = "wasi"))'.dependencies] +libc = { workspace = true } +notify = { workspace = true } + [target.'cfg(unix)'.dependencies] rustix = { workspace = true, features = ["fs"] } diff --git a/src/uu/tail/src/args.rs b/src/uu/tail/src/args.rs index e1c1fe9aee1..c1af12eddac 100644 --- a/src/uu/tail/src/args.rs +++ b/src/uu/tail/src/args.rs @@ -447,6 +447,8 @@ pub fn uu_app() -> Command { let polling_help = translate!("tail-help-polling-unix"); #[cfg(target_os = "windows")] let polling_help = translate!("tail-help-polling-windows"); + #[cfg(not(any(unix, target_os = "windows")))] + let polling_help = translate!("tail-help-polling-unix"); Command::new(uucore::util_name()) .version(uucore::crate_version!()) diff --git a/src/uu/tail/src/follow/mod.rs b/src/uu/tail/src/follow/mod.rs index 604602a4b01..71d65d4a96c 100644 --- a/src/uu/tail/src/follow/mod.rs +++ b/src/uu/tail/src/follow/mod.rs @@ -4,6 +4,69 @@ // file that was distributed with this source code. mod files; +#[cfg(not(target_os = "wasi"))] mod watch; +#[cfg(not(target_os = "wasi"))] pub use watch::{Observer, follow}; + +// WASI: notify/inotify are unavailable, so `tail -f` cannot work. +// Provide minimal stubs matching the real Observer API so tail compiles. +#[cfg(target_os = "wasi")] +mod wasi_stubs { + use crate::args::Settings; + use std::io::BufRead; + use std::path::Path; + use uucore::error::{UResult, USimpleError}; + + pub struct Observer { + pub use_polling: bool, + pub pid: super::super::platform::Pid, + } + + impl Observer { + pub fn from(settings: &Settings) -> Self { + Self { + use_polling: false, + pid: settings.pid, + } + } + + pub fn start(&mut self, _settings: &Settings) -> UResult<()> { + Ok(()) + } + + pub fn add_path( + &mut self, + _path: &Path, + _display_name: &str, + _reader: Option>, + _update_last: bool, + ) -> UResult<()> { + Ok(()) + } + + pub fn add_bad_path( + &mut self, + _path: &Path, + _display_name: &str, + _update_last: bool, + ) -> UResult<()> { + Ok(()) + } + + pub fn follow_name_retry(&self) -> bool { + false + } + } + + pub fn follow(_observer: Observer, _settings: &Settings) -> UResult<()> { + Err(USimpleError::new( + 1, + "follow mode is not supported on this platform", + )) + } +} + +#[cfg(target_os = "wasi")] +pub use wasi_stubs::{Observer, follow}; diff --git a/src/uu/tail/src/paths.rs b/src/uu/tail/src/paths.rs index 6eaeae9801b..4fd7dd51c39 100644 --- a/src/uu/tail/src/paths.rs +++ b/src/uu/tail/src/paths.rs @@ -197,6 +197,10 @@ impl MetadataExtTail for Metadata { // } false } + #[cfg(not(any(unix, windows)))] + { + false + } } } diff --git a/src/uu/tail/src/platform/mod.rs b/src/uu/tail/src/platform/mod.rs index d3220491f4a..77a9b311e25 100644 --- a/src/uu/tail/src/platform/mod.rs +++ b/src/uu/tail/src/platform/mod.rs @@ -14,6 +14,28 @@ pub use self::unix::{ #[cfg(windows)] pub use self::windows::{Pid, ProcessChecker, supports_pid_checks}; +// WASI has no process management; provide stubs so tail compiles. +#[cfg(target_os = "wasi")] +pub type Pid = u64; + +#[cfg(target_os = "wasi")] +pub struct ProcessChecker; + +#[cfg(target_os = "wasi")] +impl ProcessChecker { + pub fn new(_pid: Pid) -> Self { + Self + } + pub fn is_dead(&mut self) -> bool { + true + } +} + +#[cfg(target_os = "wasi")] +pub fn supports_pid_checks(_pid: Pid) -> bool { + false +} + #[cfg(unix)] mod unix; diff --git a/src/uu/tail/src/text.rs b/src/uu/tail/src/text.rs index 0c4972345cf..b4874643028 100644 --- a/src/uu/tail/src/text.rs +++ b/src/uu/tail/src/text.rs @@ -18,3 +18,5 @@ pub const BACKEND: &str = "inotify"; pub const BACKEND: &str = "kqueue"; #[cfg(target_os = "windows")] pub const BACKEND: &str = "ReadDirectoryChanges"; +#[cfg(not(any(unix, target_os = "windows")))] +pub const BACKEND: &str = "polling"; From 4b6ac33381528b72108805fc0374d3efd1dcd5d5 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 3 Apr 2026 12:15:13 +0200 Subject: [PATCH 157/264] uucore: add WASI support for FileInformation and io module (#11568) --- Cargo.toml | 1 + src/uu/sort/Cargo.toml | 2 +- src/uu/sort/src/tmp_dir.rs | 4 ++-- src/uucore/src/lib/features/fs.rs | 1 + src/uucore/src/lib/features/fsext.rs | 4 +++- src/uucore/src/lib/mods/io.rs | 9 ++++++++- 6 files changed, 16 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f606eae28fd..cb27c7806a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -215,6 +215,7 @@ feat_wasm = [ "shred", "shuf", "sleep", + "sort", "sum", "tail", "tee", diff --git a/src/uu/sort/Cargo.toml b/src/uu/sort/Cargo.toml index d9bc86a687e..bf7c514bf3c 100644 --- a/src/uu/sort/Cargo.toml +++ b/src/uu/sort/Cargo.toml @@ -47,7 +47,7 @@ uucore = { workspace = true, features = [ fluent = { workspace = true } foldhash = { workspace = true } -[target.'cfg(not(target_os = "redox"))'.dependencies] +[target.'cfg(not(any(target_os = "redox", target_os = "wasi")))'.dependencies] ctrlc = { workspace = true } [target.'cfg(unix)'.dependencies] diff --git a/src/uu/sort/src/tmp_dir.rs b/src/uu/sort/src/tmp_dir.rs index a9b93ceb701..cf484e2ff70 100644 --- a/src/uu/sort/src/tmp_dir.rs +++ b/src/uu/sort/src/tmp_dir.rs @@ -54,7 +54,7 @@ fn should_install_signal_handler() -> bool { open_fds.saturating_add(CTRL_C_FDS + RESERVED_FOR_MERGE) <= limit } -#[cfg(not(target_os = "redox"))] +#[cfg(not(any(target_os = "redox", target_os = "wasi")))] fn ensure_signal_handler_installed(state: Arc>) -> UResult<()> { // This shared state must originate from `HANDLER_STATE` so the handler always sees // the current lock/path pair and can clean up the active temp directory on SIGINT. @@ -104,7 +104,7 @@ fn ensure_signal_handler_installed(state: Arc>) -> UR Ok(()) } -#[cfg(target_os = "redox")] +#[cfg(any(target_os = "redox", target_os = "wasi"))] fn ensure_signal_handler_installed(_state: Arc>) -> UResult<()> { Ok(()) } diff --git a/src/uucore/src/lib/features/fs.rs b/src/uucore/src/lib/features/fs.rs index ede2d07ee7e..f56c069f55b 100644 --- a/src/uucore/src/lib/features/fs.rs +++ b/src/uucore/src/lib/features/fs.rs @@ -225,6 +225,7 @@ impl Hash for FileInformation { #[cfg(target_os = "wasi")] { self.0.len().hash(state); + self.0.file_type().is_dir().hash(state); } } } diff --git a/src/uucore/src/lib/features/fsext.rs b/src/uucore/src/lib/features/fsext.rs index 26070b108de..b118cea1fde 100644 --- a/src/uucore/src/lib/features/fsext.rs +++ b/src/uucore/src/lib/features/fsext.rs @@ -28,6 +28,7 @@ use crate::os_str_from_bytes; #[cfg(windows)] use crate::show_warning; +#[cfg(not(target_os = "wasi"))] use std::ffi::OsStr; #[cfg(unix)] use std::os::unix::ffi::OsStrExt; @@ -64,13 +65,14 @@ use libc::{ }; #[cfg(unix)] use std::ffi::{CStr, CString}; +#[cfg(not(target_os = "wasi"))] use std::io::Error as IOError; #[cfg(unix)] use std::mem; #[cfg(windows)] use std::path::Path; use std::time::SystemTime; -#[cfg(not(windows))] +#[cfg(unix)] use std::time::UNIX_EPOCH; use std::{borrow::Cow, ffi::OsString}; diff --git a/src/uucore/src/lib/mods/io.rs b/src/uucore/src/lib/mods/io.rs index a9e4a7bcbec..40bd74324fb 100644 --- a/src/uucore/src/lib/mods/io.rs +++ b/src/uucore/src/lib/mods/io.rs @@ -75,7 +75,14 @@ impl OwnedFileDescriptorOrHandle { /// instantiates a corresponding `Stdio` #[cfg(not(target_os = "wasi"))] pub fn into_stdio(self) -> Stdio { - Stdio::from(self.fx) + #[cfg(not(target_os = "wasi"))] + { + Stdio::from(self.fx) + } + #[cfg(target_os = "wasi")] + { + Stdio::from(File::from(self.fx)) + } } /// WASI: Stdio::from(OwnedFd) is not available, convert via File instead. From 7c0b682697e0e931db8160008f2b1394d801e2ef Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Thu, 2 Apr 2026 11:02:55 +0200 Subject: [PATCH 158/264] CI: run clippy on wasm32-wasip1 target with feat_wasm --- .github/workflows/code-quality.yml | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 7249ce37b6d..7a05be27234 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -74,6 +74,7 @@ jobs: - { os: ubuntu-latest , features: all , workspace: true } - { os: macos-latest , features: feat_os_unix } - { os: windows-latest , features: feat_os_windows } + - { os: ubuntu-latest , features: feat_wasm , target: wasm32-wasip1 } steps: - uses: actions/checkout@v6 with: @@ -82,6 +83,7 @@ jobs: with: toolchain: stable components: clippy + targets: ${{ matrix.job.target || '' }} - uses: Swatinem/rust-cache@v2 - name: Run sccache-cache id: sccache-setup @@ -105,6 +107,7 @@ jobs: esac; outputs FAIL_ON_FAULT FAULT_TYPE - name: Install/setup prerequisites + if: ${{ ! matrix.job.target }} shell: bash run: | ## Install/setup prerequisites @@ -138,17 +141,25 @@ jobs: extra="${extra} --workspace" ;; esac + if [[ -n "${{ matrix.job.target }}" ]]; then + extra="${extra} --no-default-features --target ${{ matrix.job.target }}" + fi # * determine sub-crate utility list (similar to FreeBSD workflow) - if [[ "${{ matrix.job.features }}" == "all" ]]; then - UTILITY_LIST="$(./util/show-utils.sh --all-features)" + if [[ -n "${{ matrix.job.target }}" ]]; then + # For cross-compilation targets, just check -pcoreutils (show-utils.sh over-resolves due to default features) + S=$(cargo clippy $extra -pcoreutils -- -D warnings 2>&1) && printf "%s\n" "$S" || { printf "%s\n" "$S" ; printf "%s" "$S" | sed -E -n -e '/^error:/{' -e "N; s/^error:[[:space:]]+(.*)\\n[[:space:]]+-->[[:space:]]+(.*):([0-9]+):([0-9]+).*$/::${fault_type} file=\2,line=\3,col=\4::${fault_prefix}: \`cargo clippy\`: \1 (file:'\2', line:\3)/p;" -e '}' ; fault=true ; } else - UTILITY_LIST="$(./util/show-utils.sh --features ${{ matrix.job.features }})" + if [[ "${{ matrix.job.features }}" == "all" ]]; then + UTILITY_LIST="$(./util/show-utils.sh --all-features)" + else + UTILITY_LIST="$(./util/show-utils.sh --features ${{ matrix.job.features }})" + fi + CARGO_UTILITY_LIST_OPTIONS="$(for u in ${UTILITY_LIST}; do echo -n "-puu_${u} "; done;)" + S=$(cargo clippy --all-targets $extra --tests --benches -pcoreutils ${CARGO_UTILITY_LIST_OPTIONS} -- -D warnings 2>&1) && printf "%s\n" "$S" || { printf "%s\n" "$S" ; printf "%s" "$S" | sed -E -n -e '/^error:/{' -e "N; s/^error:[[:space:]]+(.*)\\n[[:space:]]+-->[[:space:]]+(.*):([0-9]+):([0-9]+).*$/::${fault_type} file=\2,line=\3,col=\4::${fault_prefix}: \`cargo clippy\`: \1 (file:'\2', line:\3)/p;" -e '}' ; fault=true ; } fi - CARGO_UTILITY_LIST_OPTIONS="$(for u in ${UTILITY_LIST}; do echo -n "-puu_${u} "; done;)" - S=$(cargo clippy --all-targets $extra --tests --benches -pcoreutils ${CARGO_UTILITY_LIST_OPTIONS} -- -D warnings 2>&1) && printf "%s\n" "$S" || { printf "%s\n" "$S" ; printf "%s" "$S" | sed -E -n -e '/^error:/{' -e "N; s/^error:[[:space:]]+(.*)\\n[[:space:]]+-->[[:space:]]+(.*):([0-9]+):([0-9]+).*$/::${fault_type} file=\2,line=\3,col=\4::${fault_prefix}: \`cargo clippy\`: \1 (file:'\2', line:\3)/p;" -e '}' ; fault=true ; } if [ -n "${{ steps.vars.outputs.FAIL_ON_FAULT }}" ] && [ -n "$fault" ]; then exit 1 ; fi - name: "cargo clippy on fuzz dir" - if: runner.os != 'Windows' + if: runner.os != 'Windows' && !matrix.job.target shell: bash run: | cd fuzz From 47f1559ca32f0d35a6eb3081a6ca5bcbe9e2ec4e Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Thu, 2 Apr 2026 11:14:04 +0200 Subject: [PATCH 159/264] uucore/fs: fix unnecessary qualifications for wasm target --- src/uucore/src/lib/features/fs.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/uucore/src/lib/features/fs.rs b/src/uucore/src/lib/features/fs.rs index f56c069f55b..590d0076fed 100644 --- a/src/uucore/src/lib/features/fs.rs +++ b/src/uucore/src/lib/features/fs.rs @@ -46,7 +46,7 @@ pub struct FileInformation( #[cfg(unix)] nix::sys::stat::FileStat, #[cfg(windows)] winapi_util::file::Information, // WASI does not have nix::sys::stat, so we store std::fs::Metadata instead. - #[cfg(target_os = "wasi")] std::fs::Metadata, + #[cfg(target_os = "wasi")] fs::Metadata, ); impl FileInformation { @@ -97,9 +97,9 @@ impl FileInformation { #[cfg(target_os = "wasi")] { let metadata = if dereference { - std::fs::metadata(path.as_ref()) + fs::metadata(path.as_ref()) } else { - std::fs::symlink_metadata(path.as_ref()) + fs::symlink_metadata(path.as_ref()) }; Ok(Self(metadata?)) } From 0ec6f24ad45151b7cb785135408d8a82c3e5db5b Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Thu, 2 Apr 2026 11:18:12 +0200 Subject: [PATCH 160/264] uucore/fsext: fix unused imports and split read_fs_list for wasm target --- src/uucore/src/lib/features/fsext.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/uucore/src/lib/features/fsext.rs b/src/uucore/src/lib/features/fsext.rs index b118cea1fde..0ede0f2875f 100644 --- a/src/uucore/src/lib/features/fsext.rs +++ b/src/uucore/src/lib/features/fsext.rs @@ -428,6 +428,7 @@ fn mount_dev_id(mount_dir: &OsStr) -> String { } } +#[cfg(not(target_os = "wasi"))] use crate::error::UResult; #[cfg(any( target_os = "freebsd", @@ -458,6 +459,7 @@ use std::ptr; use std::slice; /// Read file system list. +#[cfg(not(target_os = "wasi"))] pub fn read_fs_list() -> UResult> { #[cfg(any(target_os = "linux", target_os = "android", target_os = "cygwin"))] { @@ -538,7 +540,6 @@ pub fn read_fs_list() -> UResult> { target_os = "redox", target_os = "illumos", target_os = "solaris", - target_os = "wasi" ))] { // No method to read mounts on these platforms @@ -546,6 +547,13 @@ pub fn read_fs_list() -> UResult> { } } +/// Read file system list. +#[cfg(target_os = "wasi")] +pub fn read_fs_list() -> Vec { + // No method to read mounts on WASI + Vec::new() +} + #[derive(Debug, Clone)] pub struct FsUsage { pub blocksize: u64, From 9ceabb6d93864e1537efe7661fb236a78eac48f6 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Thu, 2 Apr 2026 11:18:25 +0200 Subject: [PATCH 161/264] wc: split WordCountable trait to exclude inner_file on wasi --- src/uu/wc/src/countable.rs | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/uu/wc/src/countable.rs b/src/uu/wc/src/countable.rs index 5b2ad7a9965..32d56553879 100644 --- a/src/uu/wc/src/countable.rs +++ b/src/uu/wc/src/countable.rs @@ -20,13 +20,20 @@ pub trait WordCountable: AsFd + AsRawFd + Read { fn inner_file(&mut self) -> Option<&mut File>; } -#[cfg(not(unix))] +#[cfg(all(not(unix), not(target_os = "wasi")))] pub trait WordCountable: Read { type Buffered: BufRead; fn buffered(self) -> Self::Buffered; fn inner_file(&mut self) -> Option<&mut File>; } +#[cfg(target_os = "wasi")] +pub trait WordCountable: Read { + type Buffered: BufRead; + fn buffered(self) -> Self::Buffered; +} + +#[cfg(not(target_os = "wasi"))] impl WordCountable for StdinLock<'_> { type Buffered = Self; @@ -38,6 +45,16 @@ impl WordCountable for StdinLock<'_> { } } +#[cfg(target_os = "wasi")] +impl WordCountable for StdinLock<'_> { + type Buffered = Self; + + fn buffered(self) -> Self::Buffered { + self + } +} + +#[cfg(not(target_os = "wasi"))] impl WordCountable for File { type Buffered = BufReader; @@ -49,3 +66,12 @@ impl WordCountable for File { Some(self) } } + +#[cfg(target_os = "wasi")] +impl WordCountable for File { + type Buffered = BufReader; + + fn buffered(self) -> Self::Buffered { + BufReader::new(self) + } +} From 0b599a0b774f06f1a4470faadc485455559aa098 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Thu, 2 Apr 2026 11:30:42 +0200 Subject: [PATCH 162/264] github action: move the clippy management into a script --- .github/workflows/code-quality.yml | 35 ++----- .github/workflows/freebsd.yml | 8 +- .github/workflows/openbsd.yml | 8 +- util/run-clippy.py | 153 +++++++++++++++++++++++++++++ 4 files changed, 169 insertions(+), 35 deletions(-) create mode 100755 util/run-clippy.py diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 7a05be27234..e5bad3e1d50 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -127,37 +127,18 @@ jobs: shell: bash command: | ## `cargo clippy` lint testing - unset fault - fault_type="${{ steps.vars.outputs.FAULT_TYPE }}" - fault_prefix=$(echo "$fault_type" | tr '[:lower:]' '[:upper:]') - # * convert any warnings to GHA UI annotations; ref: - if [[ "${{ matrix.job.features }}" == "all" ]]; then - extra="--all-features" - else - extra="--features ${{ matrix.job.features }}" + ARGS="--features ${{ matrix.job.features }}" + ARGS="${ARGS} --fault-type ${{ steps.vars.outputs.FAULT_TYPE }}" + if [[ "${{ matrix.job.workspace }}" =~ ^(1|t|true|y|yes)$ ]]; then + ARGS="${ARGS} --workspace" fi - case '${{ matrix.job.workspace }}' in - 1|t|true|y|yes) - extra="${extra} --workspace" - ;; - esac if [[ -n "${{ matrix.job.target }}" ]]; then - extra="${extra} --no-default-features --target ${{ matrix.job.target }}" + ARGS="${ARGS} --target ${{ matrix.job.target }}" fi - # * determine sub-crate utility list (similar to FreeBSD workflow) - if [[ -n "${{ matrix.job.target }}" ]]; then - # For cross-compilation targets, just check -pcoreutils (show-utils.sh over-resolves due to default features) - S=$(cargo clippy $extra -pcoreutils -- -D warnings 2>&1) && printf "%s\n" "$S" || { printf "%s\n" "$S" ; printf "%s" "$S" | sed -E -n -e '/^error:/{' -e "N; s/^error:[[:space:]]+(.*)\\n[[:space:]]+-->[[:space:]]+(.*):([0-9]+):([0-9]+).*$/::${fault_type} file=\2,line=\3,col=\4::${fault_prefix}: \`cargo clippy\`: \1 (file:'\2', line:\3)/p;" -e '}' ; fault=true ; } - else - if [[ "${{ matrix.job.features }}" == "all" ]]; then - UTILITY_LIST="$(./util/show-utils.sh --all-features)" - else - UTILITY_LIST="$(./util/show-utils.sh --features ${{ matrix.job.features }})" - fi - CARGO_UTILITY_LIST_OPTIONS="$(for u in ${UTILITY_LIST}; do echo -n "-puu_${u} "; done;)" - S=$(cargo clippy --all-targets $extra --tests --benches -pcoreutils ${CARGO_UTILITY_LIST_OPTIONS} -- -D warnings 2>&1) && printf "%s\n" "$S" || { printf "%s\n" "$S" ; printf "%s" "$S" | sed -E -n -e '/^error:/{' -e "N; s/^error:[[:space:]]+(.*)\\n[[:space:]]+-->[[:space:]]+(.*):([0-9]+):([0-9]+).*$/::${fault_type} file=\2,line=\3,col=\4::${fault_prefix}: \`cargo clippy\`: \1 (file:'\2', line:\3)/p;" -e '}' ; fault=true ; } + if [[ -n "${{ steps.vars.outputs.FAIL_ON_FAULT }}" ]]; then + ARGS="${ARGS} --fail-on-fault" fi - if [ -n "${{ steps.vars.outputs.FAIL_ON_FAULT }}" ] && [ -n "$fault" ]; then exit 1 ; fi + python3 util/run-clippy.py ${ARGS} - name: "cargo clippy on fuzz dir" if: runner.os != 'Windows' && !matrix.job.target shell: bash diff --git a/.github/workflows/freebsd.yml b/.github/workflows/freebsd.yml index f963cf3ef13..f31af77adc2 100644 --- a/.github/workflows/freebsd.yml +++ b/.github/workflows/freebsd.yml @@ -41,7 +41,7 @@ jobs: sync: rsync copyback: false # We need jq and GNU coreutils to run show-utils.sh and bash to use inline shell string replacement - prepare: pkg install -y curl sudo jq coreutils bash + prepare: pkg install -y curl sudo jq coreutils bash python3 run: | ## Prepare, build, and test # implementation modelled after ref: @@ -73,7 +73,6 @@ jobs: FAULT_PREFIX=\$(echo "\${FAULT_TYPE}" | tr '[:lower:]' '[:upper:]') # * determine sub-crate utility list UTILITY_LIST="\$(./util/show-utils.sh --features ${{ matrix.job.features }})" - CARGO_UTILITY_LIST_OPTIONS="\$(for u in \${UTILITY_LIST}; do echo -n "-puu_\${u} "; done;)" ## Info # environment echo "## environment" @@ -101,8 +100,9 @@ jobs: ## cargo clippy lint testing if [ -z "\${FAULT}" ]; then echo "## cargo clippy lint testing" - # * convert any warnings to GHA UI annotations; ref: - S=\$(cargo clippy --all-targets \${CARGO_UTILITY_LIST_OPTIONS} -- -D warnings 2>&1) && printf "%s\n" "\$S" || { printf "%s\n" "\$S" ; printf "%s" "\$S" | sed -E -n -e '/^error:/{' -e "N; s/^error:[[:space:]]+(.*)\\n[[:space:]]+-->[[:space:]]+(.*):([0-9]+):([0-9]+).*\$/::\${FAULT_TYPE} file=\2,line=\3,col=\4::\${FAULT_PREFIX}: \\\`cargo clippy\\\`: \1 (file:'\2', line:\3)/p;" -e '}' ; FAULT=true ; } + CLIPPY_ARGS="--features ${{ matrix.job.features }} --fault-type \${FAULT_TYPE}" + if [ -n "\${FAIL_ON_FAULT}" ]; then CLIPPY_ARGS="\${CLIPPY_ARGS} --fail-on-fault"; fi + python3 util/run-clippy.py \${CLIPPY_ARGS} || FAULT=true fi # Clean to avoid to rsync back the files cargo clean diff --git a/.github/workflows/openbsd.yml b/.github/workflows/openbsd.yml index dea831b108c..56b10c1d746 100644 --- a/.github/workflows/openbsd.yml +++ b/.github/workflows/openbsd.yml @@ -47,7 +47,7 @@ jobs: prepare: | # Clean up disk space before installing packages df -h - pkg_add curl sudo-- jq coreutils bash rust rust-clippy rust-rustfmt llvm-- + pkg_add curl sudo-- jq coreutils bash rust rust-clippy rust-rustfmt llvm-- python3 rm -rf /usr/share/relink/* /usr/X11R6/* /usr/share/doc/* /usr/share/man/* & # Clean up package cache after installation pkg_delete -a & @@ -84,7 +84,6 @@ jobs: FAULT_PREFIX=\$(echo "\${FAULT_TYPE}" | tr '[:lower:]' '[:upper:]') # * determine sub-crate utility list UTILITY_LIST="\$(./util/show-utils.sh --features ${{ matrix.job.features }})" - CARGO_UTILITY_LIST_OPTIONS="\$(for u in \${UTILITY_LIST}; do echo -n "-puu_\${u} "; done;)" ## Info # environment echo "## environment" @@ -111,8 +110,9 @@ jobs: ## cargo clippy lint testing if [ -z "\${FAULT}" ]; then echo "## cargo clippy lint testing" - # * convert any warnings to GHA UI annotations; ref: - S=\$(cargo clippy --all-targets \${CARGO_UTILITY_LIST_OPTIONS} -- -D warnings 2>&1) && printf "%s\n" "\$S" || { printf "%s\n" "\$S" ; printf "%s" "\$S" | sed -E -n -e '/^error:/{' -e "N; s/^error:[[:space:]]+(.*)\\n[[:space:]]+-->[[:space:]]+(.*):([0-9]+):([0-9]+).*\$/::\${FAULT_TYPE} file=\2,line=\3,col=\4::\${FAULT_PREFIX}: \\\`cargo clippy\\\`: \1 (file:'\2', line:\3)/p;" -e '}' ; FAULT=true ; } + CLIPPY_ARGS="--features ${{ matrix.job.features }} --fault-type \${FAULT_TYPE}" + if [ -n "\${FAIL_ON_FAULT}" ]; then CLIPPY_ARGS="\${CLIPPY_ARGS} --fail-on-fault"; fi + python3 util/run-clippy.py \${CLIPPY_ARGS} || FAULT=true fi # Clean to avoid to rsync back the files and free up disk space cargo clean diff --git a/util/run-clippy.py b/util/run-clippy.py new file mode 100755 index 00000000000..5dcf805ba4d --- /dev/null +++ b/util/run-clippy.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: MIT +# spell-checker:ignore pcoreutils +"""Run cargo clippy with appropriate flags and emit GitHub Actions annotations.""" + +from __future__ import annotations + +import argparse +import json +import os +import re +import subprocess +import sys + + +def run_cmd( + cmd: list[str], + *, + check: bool = False, +) -> subprocess.CompletedProcess[str]: + """Run a command with UTF-8 encoding (avoids cp1252 issues on Windows).""" + env = {**os.environ, "PYTHONUTF8": "1"} + return subprocess.run( + cmd, + capture_output=True, + text=True, + encoding="utf-8", + errors="replace", + check=check, + env=env, + ) + + +def get_utility_list(features: str) -> list[str]: + """Get list of utilities from cargo metadata.""" + if features == "all": + cmd = ["cargo", "metadata", "--all-features", "--format-version", "1"] + else: + cmd = ["cargo", "metadata", "--features", features, "--format-version", "1"] + result = run_cmd(cmd, check=True) + metadata = json.loads(result.stdout) + # Find the coreutils root node and collect uu_ dependencies + utilities = [] + for node in metadata["resolve"]["nodes"]: + if re.search(r"coreutils[ @#]\d+\.\d+\.\d+", node["id"]): + for dep in node["deps"]: + # The pkg field contains the crate name (uu_), + # while name is the renamed dependency alias + pkg = dep["pkg"] + match = re.search(r"uu_(\w+)[@#]", pkg) + if match: + utilities.append(match.group(1)) + break + return sorted(utilities) + + +def build_clippy_command( + features: str, + *, + workspace: bool, + target: str | None, +) -> list[str]: + """Build the cargo clippy command line.""" + cmd = ["cargo", "clippy"] + + extra = [] + if features == "all": + extra.append("--all-features") + else: + extra.extend(["--features", features]) + + if workspace: + extra.append("--workspace") + + if target: + extra.extend(["--no-default-features", "--target", target]) + # For cross-compilation targets, just check -pcoreutils + # (show-utils.sh over-resolves due to default features) + extra.append("-pcoreutils") + else: + extra.extend(["--all-targets", "--tests", "--benches", "-pcoreutils"]) + utilities = get_utility_list(features) + extra.extend(f"-puu_{u}" for u in utilities) + + cmd.extend(extra) + cmd.extend(["--", "-D", "warnings"]) + return cmd + + +# Pattern to match clippy/rustc errors for GHA annotations +ERROR_PATTERN = re.compile( + r"^error:\s+(.*)\n\s+-->\s+(.*):(\d+):(\d+)", + re.MULTILINE, +) + + +def emit_annotations(output: str, fault_type: str) -> None: + """Emit GitHub Actions annotations from cargo clippy errors.""" + fault_prefix = fault_type.upper() + for m in ERROR_PATTERN.finditer(output): + message, file, line, col = m.groups() + print( + f"::{fault_type} file={file},line={line},col={col}" + f"::{fault_prefix}: `cargo clippy`: {message} (file:'{file}', line:{line})", + ) + + +def main() -> int: + """Run cargo clippy and emit GHA annotations on failure.""" + parser = argparse.ArgumentParser(description="Run cargo clippy for CI") + parser.add_argument("--features", required=True, help="Feature set to use") + parser.add_argument( + "--workspace", + action="store_true", + help="Include --workspace flag", + ) + parser.add_argument("--target", default=None, help="Cross-compilation target") + parser.add_argument( + "--fault-type", + default="warning", + choices=["warning", "error"], + help="GHA annotation type", + ) + parser.add_argument( + "--fail-on-fault", + action="store_true", + help="Exit with error code on clippy failures", + ) + args = parser.parse_args() + + cmd = build_clippy_command( + args.features, + workspace=args.workspace, + target=args.target, + ) + print(f"Running: {' '.join(cmd)}", file=sys.stderr) + + result = run_cmd(cmd) + output = result.stdout + result.stderr + + # Always print the full output + print(output) + + if result.returncode != 0: + emit_annotations(output, args.fault_type) + if args.fail_on_fault: + return 1 + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From 099063193d215632d623ac1d183d733c10ea3211 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Can=20B=C3=B6l=C3=BCk?= Date: Fri, 3 Apr 2026 12:20:47 +0200 Subject: [PATCH 163/264] split: preserve non-UTF-8 bytes in output filename generation (#11397) Co-authored-by: Sylvestre Ledru --- src/uu/split/src/filenames.rs | 37 +++++++++-- src/uu/split/src/platform/unix.rs | 30 +++++---- src/uu/split/src/platform/windows.rs | 23 ++++--- src/uu/split/src/split.rs | 12 ++-- tests/by-util/test_split.rs | 95 ++++++++++++++++------------ 5 files changed, 119 insertions(+), 78 deletions(-) diff --git a/src/uu/split/src/filenames.rs b/src/uu/split/src/filenames.rs index 83b43943bed..68dc35aa215 100644 --- a/src/uu/split/src/filenames.rs +++ b/src/uu/split/src/filenames.rs @@ -334,7 +334,7 @@ impl<'a> FilenameIterator<'a> { } impl Iterator for FilenameIterator<'_> { - type Item = String; + type Item = OsString; fn next(&mut self) -> Option { if self.first_iteration { @@ -344,12 +344,10 @@ impl Iterator for FilenameIterator<'_> { } // The first and third parts are just taken directly from the // struct parameters unchanged. - Some(format!( - "{}{}{}", - self.prefix.to_string_lossy(), - self.number, - self.additional_suffix.to_string_lossy() - )) + let mut filename = self.prefix.to_os_string(); + filename.push(self.number.to_string()); + filename.push(self.additional_suffix); + Some(filename) } } @@ -512,4 +510,29 @@ mod tests { let it = FilenameIterator::new(std::ffi::OsStr::new("chunk_"), &suffix); assert!(it.is_err()); } + #[test] + #[cfg(unix)] + fn test_filename_iterator_preserves_non_utf8_bytes() { + use std::ffi::OsStr; + use std::os::unix::ffi::{OsStrExt, OsStringExt}; + + let suffix = Suffix { + stype: SuffixType::Alphabetic, + length: 2, + start: 0, + auto_widening: false, + additional: std::ffi::OsString::from_vec(vec![0xFE]), + }; + + let mut it = FilenameIterator::new(OsStr::from_bytes(b"p\xFF"), &suffix) + .expect("valid fixed-width filename iterator"); + assert_eq!( + it.next().expect("first chunk filename exists").into_vec(), + b"p\xFFaa\xFE".to_vec() + ); + assert_eq!( + it.next().expect("second chunk filename exists").into_vec(), + b"p\xFFab\xFE".to_vec() + ); + } } diff --git a/src/uu/split/src/platform/unix.rs b/src/uu/split/src/platform/unix.rs index 656bd0109be..c9e0964cc31 100644 --- a/src/uu/split/src/platform/unix.rs +++ b/src/uu/split/src/platform/unix.rs @@ -3,11 +3,12 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. use std::env; -use std::ffi::OsStr; +use std::ffi::{OsStr, OsString}; use std::io::{BufWriter, Error, Result}; use std::io::{ErrorKind, Write}; use std::path::Path; use std::process::{Child, Command, Stdio}; +use uucore::display::Quotable; use uucore::error::USimpleError; use uucore::fs; use uucore::fs::FileInformation; @@ -45,12 +46,12 @@ struct WithEnvVarSet { /// Env var key previous_var_key: String, /// Previous value set to this key - previous_var_value: std::result::Result, + previous_var_value: Option, } impl WithEnvVarSet { /// Save previous value assigned to key, set key=value - fn new(key: &str, value: &str) -> Self { - let previous_env_value = env::var(key); + fn new(key: &str, value: &OsStr) -> Self { + let previous_env_value = env::var_os(key); unsafe { env::set_var(key, value); } @@ -64,7 +65,7 @@ impl WithEnvVarSet { impl Drop for WithEnvVarSet { /// Restore previous value now that this is being dropped by context fn drop(&mut self) { - if let Ok(ref prev_value) = self.previous_var_value { + if let Some(prev_value) = &self.previous_var_value { unsafe { env::set_var(&self.previous_var_key, prev_value); } @@ -82,7 +83,7 @@ impl FilterWriter { /// /// * `command` - The shell command to execute /// * `filepath` - Path of the output file (forwarded to command as $FILE) - fn new(command: &str, filepath: &str) -> Result { + fn new(command: &str, filepath: &OsStr) -> Result { // set $FILE, save previous value (if there was one) let _with_env_var_set = WithEnvVarSet::new("FILE", filepath); @@ -127,7 +128,7 @@ impl Drop for FilterWriter { /// Instantiate either a file writer or a "write to shell process's stdin" writer pub fn instantiate_current_writer( filter: Option<&str>, - filename: &str, + filename: &OsStr, is_new: bool, ) -> Result>> { match filter { @@ -138,24 +139,25 @@ pub fn instantiate_current_writer( .write(true) .create(true) .truncate(true) - .open(Path::new(&filename)) + .open(Path::new(filename)) .map_err(|e| match e.kind() { ErrorKind::IsADirectory => Error::other( - translate!("split-error-is-a-directory", "dir" => filename), + translate!("split-error-is-a-directory", "dir" => filename.quote()), ), _ => Error::other( - translate!("split-error-unable-to-open-file", "file" => filename), + translate!("split-error-unable-to-open-file", "file" => filename.quote()), ), })? } else { // re-open file that we previously created to append to it std::fs::OpenOptions::new() .append(true) - .open(Path::new(&filename)) + .open(Path::new(filename)) .map_err(|_| { - Error::other( - translate!("split-error-unable-to-reopen-file", "file" => filename), - ) + Error::other(translate!( + "split-error-unable-to-reopen-file", + "file" => filename.quote() + )) })? }; Ok(BufWriter::new(Box::new(file) as Box)) diff --git a/src/uu/split/src/platform/windows.rs b/src/uu/split/src/platform/windows.rs index 6693e4fe909..3efef2240b9 100644 --- a/src/uu/split/src/platform/windows.rs +++ b/src/uu/split/src/platform/windows.rs @@ -6,6 +6,7 @@ use std::ffi::OsStr; use std::io::{BufWriter, Error, Result}; use std::io::{ErrorKind, Write}; use std::path::Path; +use uucore::display::Quotable; use uucore::fs; use uucore::translate; @@ -15,7 +16,7 @@ use uucore::translate; /// a file writer pub fn instantiate_current_writer( _filter: Option<&str>, - filename: &str, + filename: &OsStr, is_new: bool, ) -> Result>> { let file = if is_new { @@ -24,22 +25,24 @@ pub fn instantiate_current_writer( .write(true) .create(true) .truncate(true) - .open(Path::new(&filename)) + .open(Path::new(filename)) .map_err(|e| match e.kind() { - ErrorKind::IsADirectory => { - Error::other(translate!("split-error-is-a-directory", "dir" => filename)) - } - _ => { - Error::other(translate!("split-error-unable-to-open-file", "file" => filename)) - } + ErrorKind::IsADirectory => Error::other( + translate!("split-error-is-a-directory", "dir" => filename.quote()), + ), + _ => Error::other( + translate!("split-error-unable-to-open-file", "file" => filename.quote()), + ), })? } else { // re-open file that we previously created to append to it std::fs::OpenOptions::new() .append(true) - .open(Path::new(&filename)) + .open(Path::new(filename)) .map_err(|_| { - Error::other(translate!("split-error-unable-to-reopen-file", "file" => filename)) + Error::other( + translate!("split-error-unable-to-reopen-file", "file" => filename.quote()), + ) })? }; Ok(BufWriter::new(Box::new(file) as Box)) diff --git a/src/uu/split/src/split.rs b/src/uu/split/src/split.rs index 923f66ac134..31b1ec3c9ea 100644 --- a/src/uu/split/src/split.rs +++ b/src/uu/split/src/split.rs @@ -14,7 +14,7 @@ use crate::filenames::{FilenameIterator, Suffix, SuffixError}; use crate::strategy::{NumberType, Strategy, StrategyError}; use clap::{Arg, ArgAction, ArgMatches, Command, ValueHint, parser::ValueSource}; use std::env; -use std::ffi::OsString; +use std::ffi::{OsStr, OsString}; use std::fs::{File, metadata}; use std::io; use std::io::{BufRead, BufReader, BufWriter, ErrorKind, Read, Seek, SeekFrom, Write, stdin}; @@ -540,10 +540,10 @@ impl Settings { fn instantiate_current_writer( &self, - filename: &str, + filename: &OsStr, is_new: bool, ) -> io::Result>> { - if platform::paths_refer_to_same_file(&self.input, filename.as_ref()) { + if platform::paths_refer_to_same_file(&self.input, filename) { return Err(io::Error::other( translate!("split-error-would-overwrite-input", "file" => filename.quote()), )); @@ -920,7 +920,7 @@ impl Write for LineChunkWriter<'_> { /// Output file parameters struct OutFile { - filename: String, + filename: OsString, maybe_writer: Option>>, is_new: bool, } @@ -974,7 +974,7 @@ impl ManageOutFiles for OutFiles { let maybe_writer = if is_writer_optional { None } else { - let instantiated = settings.instantiate_current_writer(filename.as_str(), true); + let instantiated = settings.instantiate_current_writer(&filename, true); // If there was an error instantiating the writer for a file, // it could be due to hitting the system limit of open files, // so record it as None and let [`get_writer`] function handle closing/re-opening @@ -1011,7 +1011,7 @@ impl ManageOutFiles for OutFiles { // might "steel" the freed fd and open a file on its side. Then it would be beneficial // if split would be able to close another fd before cancellation. 'loop1: loop { - let filename_to_open = self[idx].filename.as_str(); + let filename_to_open = &self[idx].filename; let file_to_open_is_new = self[idx].is_new; let maybe_writer = settings.instantiate_current_writer(filename_to_open, file_to_open_is_new); diff --git a/tests/by-util/test_split.rs b/tests/by-util/test_split.rs index 73402dfa7c2..aa7c73ff9b8 100644 --- a/tests/by-util/test_split.rs +++ b/tests/by-util/test_split.rs @@ -2021,61 +2021,74 @@ fn test_split_non_utf8_paths() { #[test] #[cfg(target_os = "linux")] -fn test_split_non_utf8_prefix() { - use std::os::unix::ffi::OsStrExt; +fn test_split_non_utf8_prefix_is_byte_preserving() { + use std::ffi::OsStr; + use std::os::unix::ffi::{OsStrExt, OsStringExt}; + let (at, mut ucmd) = at_and_ucmd!(); + at.write("input.txt", "AB"); - at.write("input.txt", "line1\nline2\nline3\nline4\n"); + let invalid_prefix_bytes = b"p\xFF"; + at.write("p�aa", "keep-aa"); + at.write("p�ab", "keep-ab"); - let prefix = std::ffi::OsStr::from_bytes(b"\xFF\xFE"); - ucmd.arg("input.txt").arg(prefix).succeeds(); + ucmd.args(&["-b", "1", "input.txt"]) + .arg(OsStr::from_bytes(invalid_prefix_bytes)) + .succeeds(); - // Check that split files were created (functionality works) - // The actual filename may be converted due to lossy conversion, but the command should succeed - let entries: Vec<_> = fs::read_dir(at.as_string()).unwrap().collect(); - let split_files = entries - .iter() - .filter_map(|e| e.as_ref().ok()) - .filter(|entry| { - let name = entry.file_name(); - let name_str = name.to_string_lossy(); - name_str.starts_with("�") || name_str.len() > 2 // split files should exist + let mut produced_split_files: Vec> = fs::read_dir(at.as_string()) + .expect("temporary split directory should be readable") + .filter_map(|entry| { + let entry = entry.ok()?; + let name = entry.file_name().into_vec(); + if name.starts_with(invalid_prefix_bytes) { + Some(name) + } else { + None + } }) - .count(); - assert!( - split_files > 0, - "Expected at least one split file to be created" + .collect(); + produced_split_files.sort(); + + assert_eq!( + produced_split_files, + vec![b"p\xFFaa".to_vec(), b"p\xFFab".to_vec()] ); + assert_eq!(at.read("p�aa"), "keep-aa"); + assert_eq!(at.read("p�ab"), "keep-ab"); } #[test] #[cfg(target_os = "linux")] -fn test_split_non_utf8_additional_suffix() { - use std::os::unix::ffi::OsStrExt; - let (at, mut ucmd) = at_and_ucmd!(); +fn test_split_non_utf8_additional_suffix_is_byte_preserving() { + use std::ffi::OsStr; + use std::os::unix::ffi::{OsStrExt, OsStringExt}; - at.write("input.txt", "line1\nline2\nline3\nline4\n"); + let (at, mut ucmd) = at_and_ucmd!(); + at.write("input.txt", "AB"); - let suffix = std::ffi::OsStr::from_bytes(b"\xFF\xFE"); - ucmd.args(&["input.txt", "--additional-suffix"]) - .arg(suffix) + let suffix_bytes = b"\xFF\xFE"; + ucmd.args(&["-b", "1", "input.txt", "--additional-suffix"]) + .arg(OsStr::from_bytes(suffix_bytes)) .succeeds(); - // Check that split files were created (functionality works) - // The actual filename may be converted due to lossy conversion, but the command should succeed - let entries: Vec<_> = fs::read_dir(at.as_string()).unwrap().collect(); - let split_files = entries - .iter() - .filter_map(|e| e.as_ref().ok()) - .filter(|entry| { - let name = entry.file_name(); - let name_str = name.to_string_lossy(); - name_str.ends_with("�") || name_str.starts_with('x') // split files should exist + let mut produced_with_suffix: Vec> = fs::read_dir(at.as_string()) + .expect("temporary split directory should be readable") + .filter_map(|entry| { + let entry = entry.ok()?; + let name = entry.file_name().into_vec(); + if name.starts_with(b"xa") && name.ends_with(suffix_bytes) { + Some(name) + } else { + None + } }) - .count(); - assert!( - split_files > 0, - "Expected at least one split file to be created" + .collect(); + produced_with_suffix.sort(); + + assert_eq!( + produced_with_suffix, + vec![b"xaa\xFF\xFE".to_vec(), b"xab\xFF\xFE".to_vec()] ); } @@ -2089,5 +2102,5 @@ fn test_split_directory_already_exists() { ucmd.args(&["file"]) .fails_with_code(1) .no_stdout() - .stderr_is("split: xaa: Is a directory\n"); + .stderr_is("split: 'xaa': Is a directory\n"); } From 668bd2613e3e04093b77fba0864773250908b298 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 3 Apr 2026 18:44:06 +0200 Subject: [PATCH 164/264] =?UTF-8?q?Revert=20"pre-commit:=20update=20rust-c?= =?UTF-8?q?lippy=20and=20cargo-lock-check=20to=20run=20without=20ac?= =?UTF-8?q?=E2=80=A6"=20(#11621)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit c612ad8b964b0ee8f5e1369df59ad45c5fed5757. --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 198ccb2087a..9abeb5ca476 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -40,14 +40,14 @@ repos: - id: rust-clippy name: Rust clippy description: Run cargo clippy on files included in the commit. - entry: cargo +stable clippy --frozen --workspace --all-targets --all-features -- -D warnings + entry: cargo +stable clippy --workspace --all-targets --all-features -- -D warnings pass_filenames: false types: [file, rust] language: system - id: cargo-lock-check name: Cargo.lock sync check description: Ensure Cargo.lock and fuzz/Cargo.lock are up-to-date. - entry: bash -c 'for dir in . fuzz; do ( cd "$dir" && cargo fetch --frozen --quiet ) || { echo "ERROR - $dir/Cargo.lock is out of date. Run -> cd $dir && cargo update"; exit 1; } done' + entry: bash -c 'for dir in . fuzz; do ( cd "$dir" && cargo fetch --locked --quiet ) || { echo "ERROR - $dir/Cargo.lock is out of date. Run -> cd $dir && cargo update"; exit 1; } done' pass_filenames: false files: 'Cargo\.(toml|lock)$' language: system From d856e99cbfe1bfcae093dd9f656b495a16e033d9 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Tue, 31 Mar 2026 23:03:41 +0200 Subject: [PATCH 165/264] wasm: add 22 more utilities to feat_wasm and add WASI platform stubs --- Cargo.toml | 20 +++++++++++++++++++- src/uu/ln/src/ln.rs | 8 ++++++++ src/uu/split/src/platform/mod.rs | 28 ++++++++++++++++++++++++++++ src/uu/touch/src/touch.rs | 4 ++++ src/uu/tsort/src/tsort.rs | 1 - 5 files changed, 59 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index cb27c7806a8..10b0369ca86 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -180,27 +180,36 @@ feat_Tier1 = [ # It is bit complex to deduplicate with other lists feat_wasm = [ "arch", - "basename", "base32", "base64", "basenc", + "basename", "cat", + "comm", "cp", + "csplit", "cut", "ls", "date", + "dir", "dircolors", "dirname", "echo", "expand", + "expr", "factor", "false", "fmt", "fold", + "head", "join", "link", + "ln", + "ls", + "mkdir", "mv", "nl", + "nproc", "numfmt", "od", "paste", @@ -211,7 +220,13 @@ feat_wasm = [ "printf", "ptx", "pwd", + "readlink", + "realpath", + "rm", + "rmdir", "seq", + "sort", + "split", "shred", "shuf", "sleep", @@ -219,13 +234,16 @@ feat_wasm = [ "sum", "tail", "tee", + "touch", "tr", "true", "truncate", + "tsort", "uname", "unexpand", "uniq", "unlink", + "vdir", "wc", "yes", # cksum family diff --git a/src/uu/ln/src/ln.rs b/src/uu/ln/src/ln.rs index 1c7d40b8476..f7c953908a1 100644 --- a/src/uu/ln/src/ln.rs +++ b/src/uu/ln/src/ln.rs @@ -488,3 +488,11 @@ pub fn symlink, P2: AsRef>(src: P1, dst: P2) -> std::io::R symlink_file(src, dst) } } + +#[cfg(target_os = "wasi")] +fn symlink, P2: AsRef>(_src: P1, _dst: P2) -> std::io::Result<()> { + Err(std::io::Error::new( + std::io::ErrorKind::Unsupported, + "symlinks not supported on this platform", + )) +} diff --git a/src/uu/split/src/platform/mod.rs b/src/uu/split/src/platform/mod.rs index 5afc43eeb7f..0128e7f5552 100644 --- a/src/uu/split/src/platform/mod.rs +++ b/src/uu/split/src/platform/mod.rs @@ -12,6 +12,34 @@ pub use self::windows::instantiate_current_writer; #[cfg(windows)] pub use self::windows::paths_refer_to_same_file; +// WASI: no process spawning (filter) or device/inode comparison. +#[cfg(target_os = "wasi")] +pub fn paths_refer_to_same_file(_p1: &std::ffi::OsStr, _p2: &std::ffi::OsStr) -> bool { + false +} + +#[cfg(target_os = "wasi")] +pub fn instantiate_current_writer( + _filter: Option<&str>, + filename: &str, + is_new: bool, +) -> std::io::Result>> { + let file = if is_new { + std::fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(std::path::Path::new(filename))? + } else { + std::fs::OpenOptions::new() + .append(true) + .open(std::path::Path::new(filename))? + }; + Ok(std::io::BufWriter::new( + Box::new(file) as Box + )) +} + #[cfg(unix)] mod unix; diff --git a/src/uu/touch/src/touch.rs b/src/uu/touch/src/touch.rs index d1080c1dddf..4cf4dce2299 100644 --- a/src/uu/touch/src/touch.rs +++ b/src/uu/touch/src/touch.rs @@ -883,6 +883,10 @@ fn pathbuf_from_stdout() -> Result { .map_err(|e| TouchError::WindowsStdoutPathError(e.to_string()))? .into()) } + #[cfg(target_os = "wasi")] + { + Ok(PathBuf::from("/dev/stdout")) + } } #[cfg(test)] diff --git a/src/uu/tsort/src/tsort.rs b/src/uu/tsort/src/tsort.rs index 5b2fb66d6d5..1868af85c3a 100644 --- a/src/uu/tsort/src/tsort.rs +++ b/src/uu/tsort/src/tsort.rs @@ -82,7 +82,6 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { target_os = "linux", target_os = "android", target_os = "fuchsia", - target_os = "wasi", target_env = "uclibc", target_os = "freebsd", ))] From 9f41ee2f614d698d812645d2cc068305b992ee37 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 3 Apr 2026 12:16:30 +0200 Subject: [PATCH 166/264] Remove 'expr' from dependencies in Cargo.toml (needs a c compiler) --- Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 10b0369ca86..1c63055dd78 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -196,7 +196,6 @@ feat_wasm = [ "dirname", "echo", "expand", - "expr", "factor", "false", "fmt", From 1a6ee4047a1dd464fd219fc73d691576436a593c Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Thu, 2 Apr 2026 21:36:07 +0200 Subject: [PATCH 167/264] uucore: use wasi OsStrExt/OsStringExt for byte-level access on wasm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WASI provides std::os::wasi::ffi::{OsStrExt, OsStringExt} with the same as_bytes()/from_bytes()/into_vec()/from_vec() API as Unix. Without this, os_str_as_bytes() fell into the not(unix) path requiring UTF-8 validation via to_str(), causing failures like: echo '🍎,🍌,🍒,🥝' => "invalid UTF-8 input encountered" --- src/uucore/src/lib/lib.rs | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/uucore/src/lib/lib.rs b/src/uucore/src/lib/lib.rs index 4b2cb9b502b..27548688987 100644 --- a/src/uucore/src/lib/lib.rs +++ b/src/uucore/src/lib/lib.rs @@ -142,6 +142,8 @@ use std::io::{BufRead, BufReader}; use std::iter; #[cfg(unix)] use std::os::unix::ffi::{OsStrExt, OsStringExt}; +#[cfg(target_os = "wasi")] +use std::os::wasi::ffi::{OsStrExt, OsStringExt}; use std::str; use std::str::Utf8Chunk; use std::sync::{LazyLock, atomic::Ordering}; @@ -434,12 +436,12 @@ impl error::UError for NonUtf8OsStrError {} /// /// This always succeeds on unix platforms, /// and fails on other platforms if the string can't be coerced to UTF-8. -#[cfg_attr(unix, expect(clippy::unnecessary_wraps))] +#[cfg_attr(any(unix, target_os = "wasi"), expect(clippy::unnecessary_wraps))] pub fn os_str_as_bytes(os_string: &OsStr) -> Result<&[u8], NonUtf8OsStrError> { - #[cfg(unix)] + #[cfg(any(unix, target_os = "wasi"))] return Ok(os_string.as_bytes()); - #[cfg(not(unix))] + #[cfg(not(any(unix, target_os = "wasi")))] os_string .to_str() .ok_or_else(|| NonUtf8OsStrError { @@ -453,10 +455,10 @@ pub fn os_str_as_bytes(os_string: &OsStr) -> Result<&[u8], NonUtf8OsStrError> { /// This is always lossless on unix platforms, /// and wraps [`OsStr::to_string_lossy`] on non-unix platforms. pub fn os_str_as_bytes_lossy(os_string: &OsStr) -> Cow<'_, [u8]> { - #[cfg(unix)] + #[cfg(any(unix, target_os = "wasi"))] return Cow::from(os_string.as_bytes()); - #[cfg(not(unix))] + #[cfg(not(any(unix, target_os = "wasi")))] match os_string.to_string_lossy() { Cow::Borrowed(slice) => Cow::from(slice.as_bytes()), Cow::Owned(owned) => Cow::from(owned.into_bytes()), @@ -468,12 +470,12 @@ pub fn os_str_as_bytes_lossy(os_string: &OsStr) -> Cow<'_, [u8]> { /// /// This always succeeds on unix platforms, /// and fails on other platforms if the bytes can't be parsed as UTF-8. -#[cfg_attr(unix, expect(clippy::unnecessary_wraps))] +#[cfg_attr(any(unix, target_os = "wasi"), expect(clippy::unnecessary_wraps))] pub fn os_str_from_bytes(bytes: &[u8]) -> error::UResult> { - #[cfg(unix)] + #[cfg(any(unix, target_os = "wasi"))] return Ok(Cow::Borrowed(OsStr::from_bytes(bytes))); - #[cfg(not(unix))] + #[cfg(not(any(unix, target_os = "wasi")))] Ok(Cow::Owned(OsString::from(str::from_utf8(bytes).map_err( |_| error::UUsageError::new(1, "Unable to transform bytes into OsStr"), )?))) @@ -483,12 +485,12 @@ pub fn os_str_from_bytes(bytes: &[u8]) -> error::UResult> { /// /// This always succeeds on unix platforms, /// and fails on other platforms if the bytes can't be parsed as UTF-8. -#[cfg_attr(unix, expect(clippy::unnecessary_wraps))] +#[cfg_attr(any(unix, target_os = "wasi"), expect(clippy::unnecessary_wraps))] pub fn os_string_from_vec(vec: Vec) -> error::UResult { - #[cfg(unix)] + #[cfg(any(unix, target_os = "wasi"))] return Ok(OsString::from_vec(vec)); - #[cfg(not(unix))] + #[cfg(not(any(unix, target_os = "wasi")))] Ok(OsString::from(String::from_utf8(vec).map_err(|_| { error::UUsageError::new(1, "invalid UTF-8 was detected in one or more arguments") })?)) @@ -498,11 +500,11 @@ pub fn os_string_from_vec(vec: Vec) -> error::UResult { /// /// This always succeeds on unix platforms, /// and fails on other platforms if the bytes can't be parsed as UTF-8. -#[cfg_attr(unix, expect(clippy::unnecessary_wraps))] +#[cfg_attr(any(unix, target_os = "wasi"), expect(clippy::unnecessary_wraps))] pub fn os_string_to_vec(s: OsString) -> error::UResult> { - #[cfg(unix)] + #[cfg(any(unix, target_os = "wasi"))] let v = s.into_vec(); - #[cfg(not(unix))] + #[cfg(not(any(unix, target_os = "wasi")))] let v = s .into_string() .map_err(|_| { From fa78c46cb258f5fb24c81e0b7dabd1bb3a3da230 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Thu, 2 Apr 2026 21:36:57 +0200 Subject: [PATCH 168/264] echo: add multibyte unicode test cases --- tests/by-util/test_echo.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/by-util/test_echo.rs b/tests/by-util/test_echo.rs index 3398708f176..c3c9400995d 100644 --- a/tests/by-util/test_echo.rs +++ b/tests/by-util/test_echo.rs @@ -825,4 +825,14 @@ fn test_emoji_output() { .args(&["🦀", "loves", "🚀", "and", "🌟"]) .succeeds() .stdout_only("🦀 loves 🚀 and 🌟\n"); + + new_ucmd!() + .arg("🍎,🍌,🍒,🥝") + .succeeds() + .stdout_only("🍎,🍌,🍒,🥝\n"); + + new_ucmd!() + .arg("こんにちは世界") + .succeeds() + .stdout_only("こんにちは世界\n"); } From 9e6cb795f2dd42a9aa47f97ff9670a781ad44ecf Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Thu, 2 Apr 2026 21:37:12 +0200 Subject: [PATCH 169/264] CI: add WASI test workflow with wasmtime --- .github/workflows/wasi.yml | 39 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 .github/workflows/wasi.yml diff --git a/.github/workflows/wasi.yml b/.github/workflows/wasi.yml new file mode 100644 index 00000000000..e66faa22407 --- /dev/null +++ b/.github/workflows/wasi.yml @@ -0,0 +1,39 @@ +# spell-checker:ignore wasip wasmtime +name: WASI + +# spell-checker:ignore TRIGGERPATH +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +# End the current execution if there is a new changeset in the PR. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + test_wasi: + name: Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + - uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-wasip1 + - uses: Swatinem/rust-cache@v2 + - name: Install wasmtime + run: | + curl https://wasmtime.dev/install.sh -sSf | bash + echo "$HOME/.wasmtime/bin" >> $GITHUB_PATH + - name: Run tests + env: + CARGO_TARGET_WASM32_WASIP1_RUNNER: wasmtime + run: | + cargo test --target wasm32-wasip1 --no-default-features --features feat_wasm -p uu_echo -p uu_cut From 51e495f717aa9142d8494315c93169fa3d004848 Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Thu, 2 Apr 2026 14:54:20 +0200 Subject: [PATCH 170/264] wasi: fix warnings --- src/uu/cp/src/cp.rs | 14 +++++++++----- src/uu/mv/src/mv.rs | 2 +- src/uu/wc/src/countable.rs | 4 ++++ 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index af1243779bf..ca82edf41f3 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -1896,12 +1896,13 @@ pub(crate) fn copy_attributes( fn symlink_file( source: &Path, dest: &Path, - symlinked_files: &mut HashSet, + #[cfg(not(target_os = "wasi"))] symlinked_files: &mut HashSet, + #[cfg(target_os = "wasi")] _symlinked_files: &mut HashSet, ) -> CopyResult<()> { #[cfg(target_os = "wasi")] { return Err(CpError::IoErrContext( - std::io::Error::new(std::io::ErrorKind::Unsupported, "symlinks not supported"), + io::Error::new(io::ErrorKind::Unsupported, "symlinks not supported"), translate!("cp-error-cannot-create-symlink", "dest" => get_filename(dest).unwrap_or("?").quote(), "source" => get_filename(source).unwrap_or("?").quote()), @@ -1930,10 +1931,13 @@ fn symlink_file( ) })?; } - if let Ok(file_info) = FileInformation::from_path(dest, false) { - symlinked_files.insert(file_info); + #[cfg(not(target_os = "wasi"))] + { + if let Ok(file_info) = FileInformation::from_path(dest, false) { + symlinked_files.insert(file_info); + } + Ok(()) } - Ok(()) } fn context_for(src: &Path, dest: &Path) -> String { diff --git a/src/uu/mv/src/mv.rs b/src/uu/mv/src/mv.rs index fa106ac82c0..985c4ba4a04 100644 --- a/src/uu/mv/src/mv.rs +++ b/src/uu/mv/src/mv.rs @@ -929,7 +929,7 @@ fn rename_symlink_fallback(from: &Path, to: &Path) -> io::Result<()> { } #[cfg(target_os = "wasi")] -fn rename_symlink_fallback(from: &Path, to: &Path) -> io::Result<()> { +fn rename_symlink_fallback(_from: &Path, _to: &Path) -> io::Result<()> { Err(io::Error::new( io::ErrorKind::Other, translate!("mv-error-no-symlink-support"), diff --git a/src/uu/wc/src/countable.rs b/src/uu/wc/src/countable.rs index 32d56553879..e2ff5b0e2c4 100644 --- a/src/uu/wc/src/countable.rs +++ b/src/uu/wc/src/countable.rs @@ -24,6 +24,7 @@ pub trait WordCountable: AsFd + AsRawFd + Read { pub trait WordCountable: Read { type Buffered: BufRead; fn buffered(self) -> Self::Buffered; + #[cfg(not(target_os = "wasi"))] fn inner_file(&mut self) -> Option<&mut File>; } @@ -40,6 +41,8 @@ impl WordCountable for StdinLock<'_> { fn buffered(self) -> Self::Buffered { self } + + #[cfg(not(target_os = "wasi"))] fn inner_file(&mut self) -> Option<&mut File> { None } @@ -62,6 +65,7 @@ impl WordCountable for File { BufReader::new(self) } + #[cfg(not(target_os = "wasi"))] fn inner_file(&mut self) -> Option<&mut File> { Some(self) } From d70ad129b67cf393c9ed8880f7b0178aa5d3d449 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 3 Apr 2026 12:43:31 +0200 Subject: [PATCH 171/264] sort: exclude ctrlc dependency and signal handler on wasi --- src/uu/sort/src/tmp_dir.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/uu/sort/src/tmp_dir.rs b/src/uu/sort/src/tmp_dir.rs index cf484e2ff70..0e5bd1f34d4 100644 --- a/src/uu/sort/src/tmp_dir.rs +++ b/src/uu/sort/src/tmp_dir.rs @@ -3,9 +3,9 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -#[cfg(not(target_os = "redox"))] +#[cfg(not(any(target_os = "redox", target_os = "wasi")))] use std::path::Path; -#[cfg(not(target_os = "redox"))] +#[cfg(not(any(target_os = "redox", target_os = "wasi")))] use std::sync::atomic::{AtomicBool, Ordering}; use std::{ fs::File, @@ -15,7 +15,7 @@ use std::{ use tempfile::TempDir; use uucore::error::UResult; -#[cfg(not(target_os = "redox"))] +#[cfg(not(any(target_os = "redox", target_os = "wasi")))] use uucore::{error::USimpleError, show_error, translate}; use crate::{SortError, current_open_fd_count, fd_soft_limit}; @@ -105,6 +105,7 @@ fn ensure_signal_handler_installed(state: Arc>) -> UR } #[cfg(any(target_os = "redox", target_os = "wasi"))] +#[allow(clippy::unnecessary_wraps)] fn ensure_signal_handler_installed(_state: Arc>) -> UResult<()> { Ok(()) } @@ -184,7 +185,7 @@ impl Drop for TmpDirWrapper { /// Remove the directory at `path` by deleting its child files and then itself. /// Errors while deleting child files are ignored. -#[cfg(not(target_os = "redox"))] +#[cfg(not(any(target_os = "redox", target_os = "wasi")))] fn remove_tmp_dir(path: &Path) -> std::io::Result<()> { if let Ok(read_dir) = std::fs::read_dir(path) { for file in read_dir.flatten() { From cb5fa08091d8835b3db5c5e402824b291d7c337a Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 3 Apr 2026 12:44:25 +0200 Subject: [PATCH 172/264] sort: fix clippy warnings on wasi target --- src/uu/sort/src/merge.rs | 2 ++ src/uu/sort/src/sort.rs | 12 +++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/uu/sort/src/merge.rs b/src/uu/sort/src/merge.rs index d2bb3056b92..df03a3da612 100644 --- a/src/uu/sort/src/merge.rs +++ b/src/uu/sort/src/merge.rs @@ -571,6 +571,8 @@ impl MergeInput for CompressedTmpMergeInput { type InnerRead = ChildStdout; fn finished_reading(self) -> UResult<()> { + // Explicitly close stdout before waiting on the child process. + #[allow(clippy::drop_non_drop)] drop(self.child_stdout); check_child_success(self.child, &self.compress_prog)?; let _ = fs::remove_file(self.path); diff --git a/src/uu/sort/src/sort.rs b/src/uu/sort/src/sort.rs index c3ba3dcb27a..6fe6e4ee7ff 100644 --- a/src/uu/sort/src/sort.rs +++ b/src/uu/sort/src/sort.rs @@ -37,7 +37,9 @@ use std::ffi::{OsStr, OsString}; use std::fs::{File, OpenOptions}; use std::hash::{Hash, Hasher}; use std::io::{BufRead, BufReader, BufWriter, Read, Write, stdin, stdout}; -use std::num::{IntErrorKind, NonZero}; +use std::num::IntErrorKind; +#[cfg(not(target_os = "wasi"))] +use std::num::NonZero; use std::ops::Range; #[cfg(unix)] use std::os::unix::ffi::OsStrExt; @@ -2124,12 +2126,12 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { settings.threads = matches .get_one::(options::PARALLEL) .map_or_else(|| "0".to_string(), String::from); - let num_threads = match settings.threads.parse::() { - Ok(0) | Err(_) => std::thread::available_parallelism().map_or(1, NonZero::get), - Ok(n) => n, - }; #[cfg(not(target_os = "wasi"))] { + let num_threads = match settings.threads.parse::() { + Ok(0) | Err(_) => std::thread::available_parallelism().map_or(1, NonZero::get), + Ok(n) => n, + }; let _ = rayon::ThreadPoolBuilder::new() .num_threads(num_threads) .build_global(); From cd5cc6e69cb29ea401d6b26130dda541cfb313f2 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 3 Apr 2026 12:45:09 +0200 Subject: [PATCH 173/264] cp: fix clippy warnings on wasi target --- src/uu/cp/src/cp.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index ca82edf41f3..c769feceea9 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -1901,13 +1901,12 @@ fn symlink_file( ) -> CopyResult<()> { #[cfg(target_os = "wasi")] { - return Err(CpError::IoErrContext( + Err(CpError::IoErrContext( io::Error::new(io::ErrorKind::Unsupported, "symlinks not supported"), translate!("cp-error-cannot-create-symlink", "dest" => get_filename(dest).unwrap_or("?").quote(), "source" => get_filename(source).unwrap_or("?").quote()), - ) - .into()); + )) } #[cfg(not(any(windows, target_os = "wasi")))] { From 98c60101487e3f360cf75df22805ca8985870f71 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 3 Apr 2026 12:45:46 +0200 Subject: [PATCH 174/264] mv: fix clippy warning on wasi target --- src/uu/mv/src/mv.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/uu/mv/src/mv.rs b/src/uu/mv/src/mv.rs index 985c4ba4a04..28901767e7b 100644 --- a/src/uu/mv/src/mv.rs +++ b/src/uu/mv/src/mv.rs @@ -930,10 +930,7 @@ fn rename_symlink_fallback(from: &Path, to: &Path) -> io::Result<()> { #[cfg(target_os = "wasi")] fn rename_symlink_fallback(_from: &Path, _to: &Path) -> io::Result<()> { - Err(io::Error::new( - io::ErrorKind::Other, - translate!("mv-error-no-symlink-support"), - )) + Err(io::Error::other(translate!("mv-error-no-symlink-support"))) } fn rename_dir_fallback( From 6e74247733deb48332366802991e9c990ac19efd Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 3 Apr 2026 13:18:14 +0200 Subject: [PATCH 175/264] CI: fix feat_wasm feature flag in wasi workflow --- .github/workflows/wasi.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wasi.yml b/.github/workflows/wasi.yml index e66faa22407..c4c08974de4 100644 --- a/.github/workflows/wasi.yml +++ b/.github/workflows/wasi.yml @@ -36,4 +36,4 @@ jobs: env: CARGO_TARGET_WASM32_WASIP1_RUNNER: wasmtime run: | - cargo test --target wasm32-wasip1 --no-default-features --features feat_wasm -p uu_echo -p uu_cut + cargo test --target wasm32-wasip1 --no-default-features -p uu_echo -p uu_cut From f14e4d1b6522bb46c2d3c7ed4832696fd0087796 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 3 Apr 2026 13:19:05 +0200 Subject: [PATCH 176/264] tail: fix clippy warnings on wasi target --- src/uu/tail/src/follow/mod.rs | 12 ++++++------ src/uu/tail/src/paths.rs | 11 +++++++---- src/uu/tail/src/platform/mod.rs | 13 ------------- 3 files changed, 13 insertions(+), 23 deletions(-) diff --git a/src/uu/tail/src/follow/mod.rs b/src/uu/tail/src/follow/mod.rs index 71d65d4a96c..2cd9b3af7fc 100644 --- a/src/uu/tail/src/follow/mod.rs +++ b/src/uu/tail/src/follow/mod.rs @@ -3,6 +3,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +#[cfg(not(target_os = "wasi"))] mod files; #[cfg(not(target_os = "wasi"))] mod watch; @@ -21,21 +22,19 @@ mod wasi_stubs { pub struct Observer { pub use_polling: bool, - pub pid: super::super::platform::Pid, } impl Observer { - pub fn from(settings: &Settings) -> Self { - Self { - use_polling: false, - pid: settings.pid, - } + pub fn from(_settings: &Settings) -> Self { + Self { use_polling: false } } + #[allow(clippy::unnecessary_wraps)] pub fn start(&mut self, _settings: &Settings) -> UResult<()> { Ok(()) } + #[allow(clippy::unnecessary_wraps)] pub fn add_path( &mut self, _path: &Path, @@ -46,6 +45,7 @@ mod wasi_stubs { Ok(()) } + #[allow(clippy::unnecessary_wraps)] pub fn add_bad_path( &mut self, _path: &Path, diff --git a/src/uu/tail/src/paths.rs b/src/uu/tail/src/paths.rs index 4fd7dd51c39..8682cb571af 100644 --- a/src/uu/tail/src/paths.rs +++ b/src/uu/tail/src/paths.rs @@ -12,6 +12,7 @@ use std::io::{Seek, SeekFrom}; #[cfg(unix)] use std::os::unix::fs::{FileTypeExt, MetadataExt}; use std::path::{Path, PathBuf}; +#[cfg(not(target_os = "wasi"))] use uucore::error::UResult; use uucore::translate; @@ -157,7 +158,9 @@ impl FileExtTail for File { pub trait MetadataExtTail { fn is_tailable(&self) -> bool; + #[cfg(not(target_os = "wasi"))] fn got_truncated(&self, other: &Metadata) -> UResult; + #[cfg(not(target_os = "wasi"))] fn file_id_eq(&self, other: &Metadata) -> bool; } @@ -175,10 +178,12 @@ impl MetadataExtTail for Metadata { } /// Return true if the file was modified and is now shorter + #[cfg(not(target_os = "wasi"))] fn got_truncated(&self, other: &Metadata) -> UResult { Ok(other.len() < self.len() && other.modified()? != self.modified()?) } + #[cfg(not(target_os = "wasi"))] fn file_id_eq(&self, #[cfg(unix)] other: &Metadata, #[cfg(not(unix))] _: &Metadata) -> bool { #[cfg(unix)] { @@ -197,19 +202,17 @@ impl MetadataExtTail for Metadata { // } false } - #[cfg(not(any(unix, windows)))] - { - false - } } } +#[cfg(not(target_os = "wasi"))] pub trait PathExtTail { fn is_stdin(&self) -> bool; fn is_orphan(&self) -> bool; fn is_tailable(&self) -> bool; } +#[cfg(not(target_os = "wasi"))] impl PathExtTail for Path { fn is_stdin(&self) -> bool { self.eq(Self::new(text::DASH)) diff --git a/src/uu/tail/src/platform/mod.rs b/src/uu/tail/src/platform/mod.rs index 77a9b311e25..bb77501fdcb 100644 --- a/src/uu/tail/src/platform/mod.rs +++ b/src/uu/tail/src/platform/mod.rs @@ -18,19 +18,6 @@ pub use self::windows::{Pid, ProcessChecker, supports_pid_checks}; #[cfg(target_os = "wasi")] pub type Pid = u64; -#[cfg(target_os = "wasi")] -pub struct ProcessChecker; - -#[cfg(target_os = "wasi")] -impl ProcessChecker { - pub fn new(_pid: Pid) -> Self { - Self - } - pub fn is_dead(&mut self) -> bool { - true - } -} - #[cfg(target_os = "wasi")] pub fn supports_pid_checks(_pid: Pid) -> bool { false From 50489785d01139c0e0abfb7e45c8948ec7449e8e Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 3 Apr 2026 13:19:24 +0200 Subject: [PATCH 177/264] split: fix wasi platform stub to accept OsStr filename --- src/uu/split/src/platform/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uu/split/src/platform/mod.rs b/src/uu/split/src/platform/mod.rs index 0128e7f5552..33dc874586e 100644 --- a/src/uu/split/src/platform/mod.rs +++ b/src/uu/split/src/platform/mod.rs @@ -21,7 +21,7 @@ pub fn paths_refer_to_same_file(_p1: &std::ffi::OsStr, _p2: &std::ffi::OsStr) -> #[cfg(target_os = "wasi")] pub fn instantiate_current_writer( _filter: Option<&str>, - filename: &str, + filename: &std::ffi::OsStr, is_new: bool, ) -> std::io::Result>> { let file = if is_new { From 9bca1430801668a66f6be8fc28b3338b944836ac Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 3 Apr 2026 13:19:41 +0200 Subject: [PATCH 178/264] date: fix build without i18n-datetime feature --- src/uu/date/src/date.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/uu/date/src/date.rs b/src/uu/date/src/date.rs index e577d081c49..b82b620a415 100644 --- a/src/uu/date/src/date.rs +++ b/src/uu/date/src/date.rs @@ -718,12 +718,19 @@ fn format_date_with_locale_aware_months( let broken_down = BrokenDownTime::from(date); + // When the i18n-datetime feature is enabled (default), use ICU locale-aware + // formatting if the locale requires it. Without the feature (e.g. wasi/wasm + // builds that use --no-default-features), skip localization entirely and + // format with the raw strftime string. + #[cfg(feature = "i18n-datetime")] let result = if !should_use_icu_locale() || skip_localization { broken_down.to_string_with_config(config, format_string) } else { let fmt = localize_format_string(format_string, date.date()); broken_down.to_string_with_config(config, &fmt) }; + #[cfg(not(feature = "i18n-datetime"))] + let result = broken_down.to_string_with_config(config, format_string); result.map_err(|e| e.to_string()) } From a507069c58221371a042321f92a8c56cfe90a88d Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 3 Apr 2026 13:26:44 +0200 Subject: [PATCH 179/264] ignore some CI words --- .vscode/cspell.dictionaries/jargon.wordlist.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.vscode/cspell.dictionaries/jargon.wordlist.txt b/.vscode/cspell.dictionaries/jargon.wordlist.txt index 2f26340acdc..e4c1592b475 100644 --- a/.vscode/cspell.dictionaries/jargon.wordlist.txt +++ b/.vscode/cspell.dictionaries/jargon.wordlist.txt @@ -215,6 +215,10 @@ vals inval nofield +# ci +dtolnay +Swatinem + # * clippy uninlined nonminimal From bbde36f8ac97f32143d3c40bdc5a83504d1d8309 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 3 Apr 2026 13:32:16 +0200 Subject: [PATCH 180/264] adjust the spell-checker ignores --- .github/workflows/CICD.yml | 4 ++-- .github/workflows/FixPR.yml | 2 +- .github/workflows/GnuTests.yml | 4 ++-- .github/workflows/android.yml | 2 +- .github/workflows/benchmarks.yml | 2 +- .github/workflows/code-quality.yml | 2 +- .github/workflows/documentation.yml | 2 +- .github/workflows/freebsd.yml | 2 +- .github/workflows/fuzzing.yml | 2 +- .github/workflows/make.yml | 2 +- .github/workflows/manpage-lint.yml | 2 +- .github/workflows/openbsd.yml | 2 +- 12 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index a3e1a194c01..4385ae152dc 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -3,9 +3,9 @@ name: CICD # spell-checker:ignore (abbrev/names) CACHEDIR CICD CodeCOV MacOS MinGW MSVC musl taiki # spell-checker:ignore (env/flags) Awarnings Ccodegen Coverflow Cpanic Dwarnings RUSTDOCFLAGS RUSTFLAGS Zpanic CARGOFLAGS CLEVEL nodocs # spell-checker:ignore (jargon) SHAs deps dequote softprops subshell toolchain fuzzers dedupe devel profdata -# spell-checker:ignore (people) Peltoche rivy dtolnay Anson dawidd +# spell-checker:ignore (people) Peltoche rivy Anson dawidd # spell-checker:ignore (shell/tools) binutils choco clippy dmake esac fakeroot fdesc fdescfs gmake grcov halium lcov libclang libfuse libssl limactl nextest nocross pacman popd printf pushd redoxer rsync rustc rustfmt rustup shopt sccache utmpdump xargs zstd -# spell-checker:ignore (misc) aarch alnum armhf bindir busytest coreutils defconfig DESTDIR gecos getenforce gnueabihf issuecomment maint manpages msys multisize noconfirm nofeatures nullglob onexitbegin onexitend pell runtest Swatinem tempfile testsuite toybox uutils libsystemd codspeed wasip libexecinfo +# spell-checker:ignore (misc) aarch alnum armhf bindir busytest coreutils defconfig DESTDIR gecos getenforce gnueabihf issuecomment maint manpages msys multisize noconfirm nofeatures nullglob onexitbegin onexitend pell runtest tempfile testsuite toybox uutils libsystemd codspeed wasip libexecinfo env: PROJECT_NAME: coreutils diff --git a/.github/workflows/FixPR.yml b/.github/workflows/FixPR.yml index 0024aee5883..224617c6d3e 100644 --- a/.github/workflows/FixPR.yml +++ b/.github/workflows/FixPR.yml @@ -1,6 +1,6 @@ name: FixPR -# spell-checker:ignore Swatinem dtolnay dedupe +# spell-checker:ignore dedupe # Trigger automated fixes for PRs being merged (with associated commits) diff --git a/.github/workflows/GnuTests.yml b/.github/workflows/GnuTests.yml index e063c70552a..f91941feb8d 100644 --- a/.github/workflows/GnuTests.yml +++ b/.github/workflows/GnuTests.yml @@ -1,10 +1,10 @@ name: GnuTests -# spell-checker:ignore (abbrev/names) CodeCov gnulib GnuTests Swatinem +# spell-checker:ignore (abbrev/names) CodeCov gnulib GnuTests # spell-checker:ignore (jargon) submodules devel # spell-checker:ignore (libs/utils) chksum dpkg getenforce gperf lcov libexpect limactl pyinotify setenforce shopt valgrind libattr libcap taiki-e zstd cpio # spell-checker:ignore (options) Ccodegen Coverflow Cpanic Zpanic -# spell-checker:ignore (people) Dawid Dziurla * dawidd dtolnay +# spell-checker:ignore (people) Dawid Dziurla * dawidd # spell-checker:ignore (vars) FILESET SUBDIRS XPASS # spell-checker:ignore userns nodocs diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index eeb68292d00..2837c79f60a 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -1,6 +1,6 @@ name: Android -# spell-checker:ignore (people) reactivecircus Swatinem dtolnay juliangruber +# spell-checker:ignore (people) reactivecircus juliangruber # spell-checker:ignore (shell/tools) TERMUX nextest udevadm pkill # spell-checker:ignore (misc) swiftshader playstore DATALOSS noaudio diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index d2d13275266..17c37e41a93 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -1,6 +1,6 @@ name: Benchmarks -# spell-checker:ignore (people) dtolnay Swatinem taiki-e +# spell-checker:ignore (people) taiki-e # spell-checker:ignore (misc) codspeed sccache on: diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index e5bad3e1d50..7b7bf87e06a 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -1,6 +1,6 @@ name: Code Quality -# spell-checker:ignore (people) dtolnay juliangruber pell reactivecircus Swatinem taiki-e taplo +# spell-checker:ignore (people) juliangruber pell reactivecircus taiki-e taplo # spell-checker:ignore (misc) TERMUX noaudio pkill swiftshader esac sccache pcoreutils shopt subshell dequote libsystemd on: diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 53f830a7911..ae5e8d3c8c1 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -1,4 +1,4 @@ -# spell-checker:ignore dtolnay libsystemd libattr libcap gsub +# spell-checker:ignore libsystemd libattr libcap gsub name: Check uudoc Documentation Generation diff --git a/.github/workflows/freebsd.yml b/.github/workflows/freebsd.yml index f31af77adc2..91879538877 100644 --- a/.github/workflows/freebsd.yml +++ b/.github/workflows/freebsd.yml @@ -1,6 +1,6 @@ name: FreeBSD -# spell-checker:ignore sshfs usesh vmactions taiki Swatinem esac fdescfs fdesc nextest copyback logind +# spell-checker:ignore sshfs usesh vmactions taiki esac fdescfs fdesc nextest copyback logind env: # * style job configuration diff --git a/.github/workflows/fuzzing.yml b/.github/workflows/fuzzing.yml index 034f5384f38..47ee6477191 100644 --- a/.github/workflows/fuzzing.yml +++ b/.github/workflows/fuzzing.yml @@ -1,6 +1,6 @@ name: Fuzzing -# spell-checker:ignore (people) dtolnay Swatinem taiki-e +# spell-checker:ignore (people) taiki-e # spell-checker:ignore (misc) fuzzer env: diff --git a/.github/workflows/make.yml b/.github/workflows/make.yml index 1ebe630c124..936b60669ca 100644 --- a/.github/workflows/make.yml +++ b/.github/workflows/make.yml @@ -5,7 +5,7 @@ name: make # spell-checker:ignore (jargon) deps softprops toolchain # spell-checker:ignore (people) dawidd # spell-checker:ignore (shell/tools) nextest sccache zstd -# spell-checker:ignore (misc) bindir busytest defconfig DESTDIR manpages multisize runtest Swatinem testsuite toybox uutils +# spell-checker:ignore (misc) bindir busytest defconfig DESTDIR manpages multisize runtest testsuite toybox uutils env: PROJECT_NAME: coreutils diff --git a/.github/workflows/manpage-lint.yml b/.github/workflows/manpage-lint.yml index fe30d79afa7..c294e01a26b 100644 --- a/.github/workflows/manpage-lint.yml +++ b/.github/workflows/manpage-lint.yml @@ -1,4 +1,4 @@ -# spell-checker:ignore mandoc uudoc manpages dtolnay libsystemd libattr libcap DESTDIR +# spell-checker:ignore mandoc uudoc manpages libsystemd libattr libcap DESTDIR name: Manpage Validation diff --git a/.github/workflows/openbsd.yml b/.github/workflows/openbsd.yml index 56b10c1d746..797185e04c9 100644 --- a/.github/workflows/openbsd.yml +++ b/.github/workflows/openbsd.yml @@ -1,6 +1,6 @@ name: OpenBSD -# spell-checker:ignore sshfs usesh vmactions taiki Swatinem esac fdescfs fdesc sccache nextest copyback logind bindgen libclang +# spell-checker:ignore sshfs usesh vmactions taiki esac fdescfs fdesc sccache nextest copyback logind bindgen libclang env: # * style job configuration From 4236e4547f90ba6d64bbbbfe77e0fe493796050d Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 3 Apr 2026 17:57:02 +0200 Subject: [PATCH 181/264] wc: remove unnecessary cfg --- src/uu/wc/src/countable.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/uu/wc/src/countable.rs b/src/uu/wc/src/countable.rs index e2ff5b0e2c4..32d56553879 100644 --- a/src/uu/wc/src/countable.rs +++ b/src/uu/wc/src/countable.rs @@ -24,7 +24,6 @@ pub trait WordCountable: AsFd + AsRawFd + Read { pub trait WordCountable: Read { type Buffered: BufRead; fn buffered(self) -> Self::Buffered; - #[cfg(not(target_os = "wasi"))] fn inner_file(&mut self) -> Option<&mut File>; } @@ -41,8 +40,6 @@ impl WordCountable for StdinLock<'_> { fn buffered(self) -> Self::Buffered { self } - - #[cfg(not(target_os = "wasi"))] fn inner_file(&mut self) -> Option<&mut File> { None } @@ -65,7 +62,6 @@ impl WordCountable for File { BufReader::new(self) } - #[cfg(not(target_os = "wasi"))] fn inner_file(&mut self) -> Option<&mut File> { Some(self) } From 63c8319e806b1b49f16154a5fad4f41082373823 Mon Sep 17 00:00:00 2001 From: can1357 Date: Wed, 18 Mar 2026 23:23:58 +0100 Subject: [PATCH 182/264] od: default `-t f` to double precision --- src/uu/od/src/parse_formats.rs | 7 +++--- tests/by-util/test_od.rs | 46 ++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/src/uu/od/src/parse_formats.rs b/src/uu/od/src/parse_formats.rs index 1103b77cac3..e3312b87603 100644 --- a/src/uu/od/src/parse_formats.rs +++ b/src/uu/od/src/parse_formats.rs @@ -86,7 +86,8 @@ fn od_format_type(type_char: FormatType, byte_size: u8) -> Option Some(FORMAT_ITEM_HEX64), (FormatType::Float, 2) => Some(FORMAT_ITEM_F16), - (FormatType::Float, 0 | 4) => Some(FORMAT_ITEM_F32), + (FormatType::Float, 4) => Some(FORMAT_ITEM_F32), + (FormatType::Float, 0) => Some(FORMAT_ITEM_F64), (FormatType::Float, 8) => Some(FORMAT_ITEM_F64), (FormatType::Float, 16) => Some(FORMAT_ITEM_LONG_DOUBLE), @@ -521,7 +522,7 @@ fn test_long_format_x_default() { fn test_long_format_f_default() { assert_eq!( parse_format_flags_str(&["od", "--format=f"]).unwrap(), - vec![FORMAT_ITEM_F32] + vec![FORMAT_ITEM_F64] ); } @@ -587,7 +588,7 @@ fn test_mixed_formats() { ParsedFormatterItemInfo::new(FORMAT_ITEM_HEX8, false), // tx1 ParsedFormatterItemInfo::new(FORMAT_ITEM_DEC16U, false), // tu2 ParsedFormatterItemInfo::new(FORMAT_ITEM_C, false), // tc - ParsedFormatterItemInfo::new(FORMAT_ITEM_F32, false), // tf + ParsedFormatterItemInfo::new(FORMAT_ITEM_F64, false), // tf ParsedFormatterItemInfo::new(FORMAT_ITEM_HEX16, false), // x ] ); diff --git a/tests/by-util/test_od.rs b/tests/by-util/test_od.rs index 21cef8404ea..823f2d1dc51 100644 --- a/tests/by-util/test_od.rs +++ b/tests/by-util/test_od.rs @@ -223,6 +223,52 @@ fn test_bfloat16_compact() { .stdout_only(" 1 1\n"); } +#[test] +fn test_tf_default_is_double() { + let input: [u8; 8] = [0x00, 0x00, 0x80, 0x3f, 0x00, 0x00, 0x00, 0x40]; + let default_output = new_ucmd!() + .arg("--endian=little") + .arg("-An") + .arg("-tf") + .run_piped_stdin(&input[..]) + .success() + .stdout_str() + .to_string(); + + let explicit_double_output = new_ucmd!() + .arg("--endian=little") + .arg("-An") + .arg("-tfD") + .run_piped_stdin(&input[..]) + .success() + .stdout_str() + .to_string(); + + let explicit_float_output = new_ucmd!() + .arg("--endian=little") + .arg("-An") + .arg("-tfF") + .run_piped_stdin(&input[..]) + .success() + .stdout_str() + .to_string(); + + assert_eq!(default_output, explicit_double_output); + assert_ne!(default_output, explicit_float_output); +} + +#[test] +fn test_tf_explicit_float_still_uses_4_bytes() { + let input: [u8; 8] = [0x00, 0x00, 0x80, 0x3f, 0x00, 0x00, 0x00, 0x40]; + new_ucmd!() + .arg("--endian=little") + .arg("-An") + .arg("-tfF") + .run_piped_stdin(&input[..]) + .success() + .stdout_only(" 1.0000000 2.0000000\n"); +} + #[test] fn test_f16() { let input: [u8; 14] = [ From 60d18a84db9617a68f94e0511b4feda114e8b235 Mon Sep 17 00:00:00 2001 From: xtqqczze <45661989+xtqqczze@users.noreply.github.com> Date: Fri, 3 Apr 2026 20:45:29 +0100 Subject: [PATCH 183/264] chore(deps): update rust crate libredox to 0.1.15 --- Cargo.lock | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 84b43ce6320..4cff737eed3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1871,12 +1871,13 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.12" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08" dependencies = [ "bitflags 2.11.0", "libc", + "plain", "redox_syscall 0.7.0", ] @@ -2292,6 +2293,12 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + [[package]] name = "platform-info" version = "2.1.0" From 20e37cd19d9ee456b03840aca17c71304a8f01d3 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Sat, 4 Apr 2026 09:48:06 +0900 Subject: [PATCH 184/264] uucore: pipes.rs replace != with > --- src/uucore/src/lib/features/pipes.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uucore/src/lib/features/pipes.rs b/src/uucore/src/lib/features/pipes.rs index 2615912c08d..89cff07ab27 100644 --- a/src/uucore/src/lib/features/pipes.rs +++ b/src/uucore/src/lib/features/pipes.rs @@ -56,7 +56,7 @@ pub fn splice(source: &impl AsFd, target: &impl AsFd, len: usize) -> std::io::Re #[cfg(any(target_os = "linux", target_os = "android"))] pub fn splice_exact(source: &impl AsFd, target: &impl AsFd, len: usize) -> std::io::Result<()> { let mut left = len; - while left != 0 { + while left > 0 { let written = splice(source, target, left)?; debug_assert_ne!(written, 0, "unexpected end of data"); left -= written; From f8ef6ffd75d5a688547b2ccdf3e263e65ceaec0c Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 3 Apr 2026 20:51:34 +0200 Subject: [PATCH 185/264] sort: add WASI single-threaded sort path --- src/uu/sort/src/chunks.rs | 31 ++++++++++++++++++++++++++- src/uu/sort/src/ext_sort.rs | 42 +++++++++++++++++++++++++++++++++++++ src/uu/sort/src/sort.rs | 21 ++++++++++++++----- 3 files changed, 88 insertions(+), 6 deletions(-) diff --git a/src/uu/sort/src/chunks.rs b/src/uu/sort/src/chunks.rs index c808c622b81..ce6b2118be4 100644 --- a/src/uu/sort/src/chunks.rs +++ b/src/uu/sort/src/chunks.rs @@ -47,7 +47,7 @@ pub struct ChunkContents<'a> { pub line_count_hint: usize, } -#[derive(Debug)] +#[derive(Debug, Default)] pub struct LineData<'a> { pub selections: Vec<&'a [u8]>, pub num_infos: Vec, @@ -431,3 +431,32 @@ fn read_to_buffer( } } } + +/// Parse a buffer into a `ChunkContents` suitable for `Chunk::try_new`. +/// Used by the WASI single-threaded sort path. +#[cfg(target_os = "wasi")] +pub fn parse_into_chunk<'a>( + buffer: &'a [u8], + separator: u8, + settings: &GlobalSettings, +) -> ChunkContents<'a> { + let mut lines = Vec::new(); + let mut line_data = LineData::default(); + let mut token_buffer = Vec::new(); + let mut line_count_hint = 0; + parse_lines( + buffer, + &mut lines, + &mut line_data, + &mut token_buffer, + &mut line_count_hint, + separator, + settings, + ); + ChunkContents { + lines, + line_data, + token_buffer, + line_count_hint, + } +} diff --git a/src/uu/sort/src/ext_sort.rs b/src/uu/sort/src/ext_sort.rs index 59bf18a41a5..2fc6bd97ee1 100644 --- a/src/uu/sort/src/ext_sort.rs +++ b/src/uu/sort/src/ext_sort.rs @@ -40,6 +40,48 @@ use crate::{Line, print_sorted}; const DEFAULT_BUF_SIZE: usize = 8 * 1024; /// Sort files by using auxiliary files for storing intermediate chunks (if needed), and output the result. +/// +/// On WASI, threads are not available, so we read all input into memory, +/// sort in a single thread, and output directly. +#[cfg(target_os = "wasi")] +pub fn ext_sort( + files: &mut impl Iterator>>, + settings: &GlobalSettings, + output: Output, + _tmp_dir: &mut TmpDirWrapper, +) -> UResult<()> { + let separator = settings.line_ending.into(); + // Read all input into memory at once. Unlike the threaded path which uses + // chunked buffered reads, WASI has no threads so we accept the memory cost. + let mut input = Vec::new(); + for file in files { + file?.read_to_end(&mut input)?; + } + if input.is_empty() { + return Ok(()); + } + let mut chunk = chunks::Chunk::try_new(input, |buffer| { + chunks::parse_into_chunk(buffer, separator, settings) + })?; + chunk.with_dependent_mut(|_, contents| { + sort_by(&mut contents.lines, settings, &contents.line_data); + }); + if settings.unique { + print_sorted( + chunk.lines().iter().dedup_by(|a, b| { + compare_by(a, b, settings, chunk.line_data(), chunk.line_data()) == Ordering::Equal + }), + settings, + output, + )?; + } else { + print_sorted(chunk.lines().iter(), settings, output)?; + } + Ok(()) +} + +/// Sort files by using auxiliary files for storing intermediate chunks (if needed), and output the result. +#[cfg(not(target_os = "wasi"))] pub fn ext_sort( files: &mut impl Iterator>>, settings: &GlobalSettings, diff --git a/src/uu/sort/src/sort.rs b/src/uu/sort/src/sort.rs index 6fe6e4ee7ff..02822cee56d 100644 --- a/src/uu/sort/src/sort.rs +++ b/src/uu/sort/src/sort.rs @@ -2148,11 +2148,22 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { settings.buffer_size_is_explicit = false; } - let mut tmp_dir = TmpDirWrapper::new( - matches - .get_one::(options::TMP_DIR) - .map_or_else(env::temp_dir, PathBuf::from), - ); + let mut tmp_dir = TmpDirWrapper::new(matches.get_one::(options::TMP_DIR).map_or_else( + || { + // WASI does not support std::env::temp_dir() — it panics with + // "no filesystem on wasm". Use /tmp as a nominal fallback; + // the WASI ext_sort path never actually creates temp files. + #[cfg(target_os = "wasi")] + { + PathBuf::from("/tmp") + } + #[cfg(not(target_os = "wasi"))] + { + env::temp_dir() + } + }, + PathBuf::from, + )); settings.compress_prog = matches .get_one::(options::COMPRESS_PROG) From f2243353ed17da4bbb0c6add038f3012be56b9cb Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 3 Apr 2026 21:01:05 +0200 Subject: [PATCH 186/264] sort: split ext_sort into platform modules and enable WASI CI --- .github/workflows/wasi.yml | 2 +- src/uu/sort/src/ext_sort/mod.rs | 20 ++++++ .../src/{ext_sort.rs => ext_sort/threaded.rs} | 65 +++---------------- src/uu/sort/src/ext_sort/wasi.rs | 59 +++++++++++++++++ 4 files changed, 90 insertions(+), 56 deletions(-) create mode 100644 src/uu/sort/src/ext_sort/mod.rs rename src/uu/sort/src/{ext_sort.rs => ext_sort/threaded.rs} (83%) create mode 100644 src/uu/sort/src/ext_sort/wasi.rs diff --git a/.github/workflows/wasi.yml b/.github/workflows/wasi.yml index c4c08974de4..854e75422f7 100644 --- a/.github/workflows/wasi.yml +++ b/.github/workflows/wasi.yml @@ -36,4 +36,4 @@ jobs: env: CARGO_TARGET_WASM32_WASIP1_RUNNER: wasmtime run: | - cargo test --target wasm32-wasip1 --no-default-features -p uu_echo -p uu_cut + cargo test --target wasm32-wasip1 --no-default-features -p uu_echo -p uu_cut -p uu_sort diff --git a/src/uu/sort/src/ext_sort/mod.rs b/src/uu/sort/src/ext_sort/mod.rs new file mode 100644 index 00000000000..099a4b72e62 --- /dev/null +++ b/src/uu/sort/src/ext_sort/mod.rs @@ -0,0 +1,20 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +//! External sort: sort large inputs that may not fit in memory. +//! +//! On most platforms this uses a multi-threaded chunked approach with +//! temporary files. On WASI (no threads) we fall back to an in-memory sort. + +#[cfg(not(target_os = "wasi"))] +mod threaded; +#[cfg(not(target_os = "wasi"))] +pub use threaded::ext_sort; + +#[cfg(target_os = "wasi")] +mod wasi; +#[cfg(target_os = "wasi")] +// `self::` needed to disambiguate from the `wasi` crate +pub use self::wasi::ext_sort; diff --git a/src/uu/sort/src/ext_sort.rs b/src/uu/sort/src/ext_sort/threaded.rs similarity index 83% rename from src/uu/sort/src/ext_sort.rs rename to src/uu/sort/src/ext_sort/threaded.rs index 2fc6bd97ee1..7dd089d0fe8 100644 --- a/src/uu/sort/src/ext_sort.rs +++ b/src/uu/sort/src/ext_sort/threaded.rs @@ -3,21 +3,15 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -//! Sort big files by using auxiliary files for storing intermediate chunks. -//! -//! Files are read into chunks of memory which are then sorted individually and -//! written to temporary files. There are two threads: One sorter, and one reader/writer. -//! The buffers for the individual chunks are recycled. There are two buffers. +//! Threaded external sort: read input in chunks, sort them in a background +//! thread, and spill to temporary files when memory is exceeded. use std::cmp::Ordering; use std::fs::File; -use std::io::{Write, stderr}; +use std::io::{Read, Write, stderr}; use std::path::PathBuf; -use std::{ - io::Read, - sync::mpsc::{Receiver, SyncSender}, - thread, -}; +use std::sync::mpsc::{Receiver, SyncSender}; +use std::thread; use itertools::Itertools; use uucore::error::{UResult, strip_errno}; @@ -29,11 +23,10 @@ use crate::merge::WriteablePlainTmpFile; use crate::merge::WriteableTmpFile; use crate::tmp_dir::TmpDirWrapper; use crate::{ - GlobalSettings, + GlobalSettings, Line, chunks::{self, Chunk}, - compare_by, merge, sort_by, + compare_by, merge, print_sorted, sort_by, }; -use crate::{Line, print_sorted}; // Note: update `test_sort::test_start_buffer` if this size is changed // Fixed to 8 KiB (equivalent to `std::sys::io::DEFAULT_BUF_SIZE` on most targets) @@ -41,47 +34,9 @@ const DEFAULT_BUF_SIZE: usize = 8 * 1024; /// Sort files by using auxiliary files for storing intermediate chunks (if needed), and output the result. /// -/// On WASI, threads are not available, so we read all input into memory, -/// sort in a single thread, and output directly. -#[cfg(target_os = "wasi")] -pub fn ext_sort( - files: &mut impl Iterator>>, - settings: &GlobalSettings, - output: Output, - _tmp_dir: &mut TmpDirWrapper, -) -> UResult<()> { - let separator = settings.line_ending.into(); - // Read all input into memory at once. Unlike the threaded path which uses - // chunked buffered reads, WASI has no threads so we accept the memory cost. - let mut input = Vec::new(); - for file in files { - file?.read_to_end(&mut input)?; - } - if input.is_empty() { - return Ok(()); - } - let mut chunk = chunks::Chunk::try_new(input, |buffer| { - chunks::parse_into_chunk(buffer, separator, settings) - })?; - chunk.with_dependent_mut(|_, contents| { - sort_by(&mut contents.lines, settings, &contents.line_data); - }); - if settings.unique { - print_sorted( - chunk.lines().iter().dedup_by(|a, b| { - compare_by(a, b, settings, chunk.line_data(), chunk.line_data()) == Ordering::Equal - }), - settings, - output, - )?; - } else { - print_sorted(chunk.lines().iter(), settings, output)?; - } - Ok(()) -} - -/// Sort files by using auxiliary files for storing intermediate chunks (if needed), and output the result. -#[cfg(not(target_os = "wasi"))] +/// Two threads cooperate: one reads input and writes temporary chunk files, +/// while the other sorts each chunk in memory. Once all chunks are written, +/// they are merged back together for final output. pub fn ext_sort( files: &mut impl Iterator>>, settings: &GlobalSettings, diff --git a/src/uu/sort/src/ext_sort/wasi.rs b/src/uu/sort/src/ext_sort/wasi.rs new file mode 100644 index 00000000000..50bd5f63033 --- /dev/null +++ b/src/uu/sort/src/ext_sort/wasi.rs @@ -0,0 +1,59 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +//! WASI single-threaded sort: read all input into memory, sort, and output. +//! Threads are not available on WASI, so we bypass the chunked/threaded path. + +use std::cmp::Ordering; +use std::io::Read; + +use itertools::Itertools; +use uucore::error::UResult; + +use crate::Output; +use crate::chunks::{self, Chunk}; +use crate::tmp_dir::TmpDirWrapper; +use crate::{GlobalSettings, compare_by, print_sorted, sort_by}; + +/// Sort files by reading all input into memory, sorting in a single thread, and outputting directly. +pub fn ext_sort( + files: &mut impl Iterator>>, + settings: &GlobalSettings, + output: Output, + _tmp_dir: &mut TmpDirWrapper, +) -> UResult<()> { + let separator = settings.line_ending.into(); + // Read all input into memory at once. Unlike the threaded path which uses + // chunked buffered reads, WASI has no threads so we accept the memory cost. + // Note: there is no size limit here — WASI targets are expected to handle + // moderately sized inputs; very large files may cause OOM. + let mut input = Vec::new(); + for file in files { + file?.read_to_end(&mut input)?; + } + if input.is_empty() { + return Ok(()); + } + let mut chunk = Chunk::try_new(input, |buffer| { + Ok::<_, Box>(chunks::parse_into_chunk( + buffer, separator, settings, + )) + })?; + chunk.with_dependent_mut(|_, contents| { + sort_by(&mut contents.lines, settings, &contents.line_data); + }); + if settings.unique { + print_sorted( + chunk.lines().iter().dedup_by(|a, b| { + compare_by(a, b, settings, chunk.line_data(), chunk.line_data()) == Ordering::Equal + }), + settings, + output, + )?; + } else { + print_sorted(chunk.lines().iter(), settings, output)?; + } + Ok(()) +} From 3ef5bf77c530dd4ecc4cb944b798158f2640b0f3 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 3 Apr 2026 21:53:08 +0200 Subject: [PATCH 187/264] CI/WASI: test all compiling utilities instead of just 3 --- .github/workflows/wasi.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/wasi.yml b/.github/workflows/wasi.yml index 854e75422f7..ba5e5ac3472 100644 --- a/.github/workflows/wasi.yml +++ b/.github/workflows/wasi.yml @@ -36,4 +36,7 @@ jobs: env: CARGO_TARGET_WASM32_WASIP1_RUNNER: wasmtime run: | - cargo test --target wasm32-wasip1 --no-default-features -p uu_echo -p uu_cut -p uu_sort + # Get all utilities and exclude ones that don't compile for wasm32-wasip1 + EXCLUDE="dd|df|du|env|expr|mktemp|more|tac|test" + UTILS=$(./util/show-utils.sh | tr ' ' '\n' | grep -vE "^($EXCLUDE)$" | sed 's/^/-p uu_/' | tr '\n' ' ') + cargo test --target wasm32-wasip1 --no-default-features $UTILS From 7b136d8d3a3a075155a450b793c2098d26d513f7 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Sat, 4 Apr 2026 08:27:31 +0900 Subject: [PATCH 188/264] cat: simplify splice & fix comment --- src/uu/cat/src/splice.rs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/uu/cat/src/splice.rs b/src/uu/cat/src/splice.rs index 14e88dd6e44..c2fceaa9083 100644 --- a/src/uu/cat/src/splice.rs +++ b/src/uu/cat/src/splice.rs @@ -23,9 +23,9 @@ pub(super) fn write_fast_using_splice( handle: &InputHandle, write_fd: &S, ) -> CatResult { - const FIRST_PIPE_SIZE: usize = 64 * 1024; - if splice(&handle.reader, &write_fd, FIRST_PIPE_SIZE).is_ok() { - // fcntl improves performance for large file which is large overhead for small files + if splice(&handle.reader, &write_fd, MAX_ROOTLESS_PIPE_SIZE).is_ok() { + // fcntl improves throughput + // todo: avoid fcntl overhead for small input, but don't fcntl inside of the loop let _ = rustix::pipe::fcntl_setpipe_size(write_fd, MAX_ROOTLESS_PIPE_SIZE); loop { match splice(&handle.reader, &write_fd, MAX_ROOTLESS_PIPE_SIZE) { @@ -35,14 +35,12 @@ pub(super) fn write_fast_using_splice( } } } else { - // output is not pipe. Needs broker to use splice() which is high cost for small files + // both of in/output are not pipe. needs broker to use splice() with additional costs let (pipe_rd, pipe_wr) = pipe()?; loop { match splice(&handle.reader, &pipe_wr, MAX_ROOTLESS_PIPE_SIZE) { + Ok(0) => return Ok(false), Ok(n) => { - if n == 0 { - return Ok(false); - } if splice_exact(&pipe_rd, write_fd, n).is_err() { // If the first splice manages to copy to the intermediate // pipe, but the second splice to stdout fails for some reason @@ -53,9 +51,7 @@ pub(super) fn write_fast_using_splice( return Ok(true); } } - Err(_) => { - return Ok(true); - } + Err(_) => return Ok(true), } } } From c9906ee889fa501713335ab4f444cffb91f273c4 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Fri, 3 Apr 2026 02:43:23 +0900 Subject: [PATCH 189/264] uucore: add a function returning /dev/null to use splice() for wc,dd,tail --- src/uu/wc/src/count_fast.rs | 15 +-------------- src/uucore/src/lib/features/pipes.rs | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/src/uu/wc/src/count_fast.rs b/src/uu/wc/src/count_fast.rs index 247ef7d297f..ea334a8e72e 100644 --- a/src/uu/wc/src/count_fast.rs +++ b/src/uu/wc/src/count_fast.rs @@ -9,8 +9,6 @@ use uucore::hardware::SimdPolicy; use super::WordCountable; -#[cfg(any(target_os = "linux", target_os = "android"))] -use std::fs::OpenOptions; use std::io::{self, ErrorKind, Read}; #[cfg(unix)] @@ -41,18 +39,7 @@ const BUF_SIZE: usize = 256 * 1024; #[inline] #[cfg(any(target_os = "linux", target_os = "android"))] fn count_bytes_using_splice(fd: &impl AsFd) -> Result { - let null_file = OpenOptions::new() - .write(true) - .open("/dev/null") - .map_err(|_| 0_usize)?; - let null_rdev = rustix::fs::fstat(null_file.as_fd()) - .map_err(|_| 0_usize)? - .st_rdev as libc::dev_t; - if (libc::major(null_rdev), libc::minor(null_rdev)) != (1, 3) { - // This is not a proper /dev/null, writing to it is probably bad - // Bit of an edge case, but it has been known to happen - return Err(0); - } + let null_file = uucore::pipes::dev_null().ok_or(0_usize)?; // todo: avoid generating broker if input is pipe (fcntl_setpipe_size succeed) and directly splice() to /dev/null to save RAM usage let (pipe_rd, pipe_wr) = pipe().map_err(|_| 0_usize)?; diff --git a/src/uucore/src/lib/features/pipes.rs b/src/uucore/src/lib/features/pipes.rs index 89cff07ab27..c4fc19b2123 100644 --- a/src/uucore/src/lib/features/pipes.rs +++ b/src/uucore/src/lib/features/pipes.rs @@ -18,6 +18,7 @@ pub const MAX_ROOTLESS_PIPE_SIZE: usize = 1024 * 1024; /// Returns two `File` objects: everything written to the second can be read /// from the first. /// This is used only for resolving the limitation for splice: one of a input or output should be pipe +#[inline] #[cfg(any(target_os = "linux", target_os = "android"))] pub fn pipe() -> std::io::Result<(File, File)> { let (read, write) = rustix::pipe::pipe()?; @@ -36,6 +37,7 @@ pub fn pipe() -> std::io::Result<(File, File)> { /// To get around this requirement, consider splicing from your source into /// a [`pipe`] and then from the pipe into your target (with `splice_exact`): /// this is still very efficient. +#[inline] #[cfg(any(target_os = "linux", target_os = "android"))] pub fn splice(source: &impl AsFd, target: &impl AsFd, len: usize) -> std::io::Result { Ok(rustix::pipe::splice( @@ -53,6 +55,7 @@ pub fn splice(source: &impl AsFd, target: &impl AsFd, len: usize) -> std::io::Re /// Exactly `len` bytes are moved from `source` into `target`. /// /// Panics if `source` runs out of data before `len` bytes have been moved. +#[inline] #[cfg(any(target_os = "linux", target_os = "android"))] pub fn splice_exact(source: &impl AsFd, target: &impl AsFd, len: usize) -> std::io::Result<()> { let mut left = len; @@ -63,3 +66,22 @@ pub fn splice_exact(source: &impl AsFd, target: &impl AsFd, len: usize) -> std:: } Ok(()) } + +/// Return verified /dev/null +/// +/// `splice` to /dev/null is faster than `read` when we skip or count the input which is not able to seek +#[inline] +#[cfg(any(target_os = "linux", target_os = "android"))] +pub fn dev_null() -> Option { + let null = std::fs::OpenOptions::new() + .write(true) + .open("/dev/null") + .ok()?; + let stat = rustix::fs::fstat(&null).ok()?; + let dev = stat.st_rdev; + if (rustix::fs::major(dev), rustix::fs::minor(dev)) == (1, 3) { + Some(null) + } else { + None + } +} From 532fcac432427a1865a23242a2b334a1efeaaded Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sat, 4 Apr 2026 10:08:54 +0200 Subject: [PATCH 190/264] Add expand/bounded-memory to ignore list --- .github/workflows/ignore-intermittent.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ignore-intermittent.txt b/.github/workflows/ignore-intermittent.txt index 1ccd775b3e3..c09fc2dd5f0 100644 --- a/.github/workflows/ignore-intermittent.txt +++ b/.github/workflows/ignore-intermittent.txt @@ -1,6 +1,7 @@ tests/cut/bounded-memory tests/date/date-locale-hour tests/date/resolution +tests/expand/bounded-memory tests/pr/bounded-memory tests/tail/inotify-dir-recreate tests/tail/overlay-headers From 587b7f3e7b398a714cbe42a70cd4021fb7c01415 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Fri, 6 Mar 2026 17:42:33 +0900 Subject: [PATCH 191/264] libstdbuf: remove eprintln! causing 2>/dev/full abort --- src/uu/stdbuf/src/libstdbuf/src/libstdbuf.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/uu/stdbuf/src/libstdbuf/src/libstdbuf.rs b/src/uu/stdbuf/src/libstdbuf/src/libstdbuf.rs index deb8ce7ff4a..d22b8c871f4 100644 --- a/src/uu/stdbuf/src/libstdbuf/src/libstdbuf.rs +++ b/src/uu/stdbuf/src/libstdbuf/src/libstdbuf.rs @@ -6,8 +6,8 @@ use ctor::ctor; use libc::{_IOFBF, _IOLBF, _IONBF, FILE, c_char, c_int, fileno, size_t}; -use std::env; -use std::ptr; +use std::io::{Write, stderr}; +use std::{env, ptr}; // This runs automatically when the library is loaded via LD_PRELOAD #[ctor] @@ -210,7 +210,7 @@ fn set_buffer(stream: *mut FILE, value: &str) { "L" => (_IOLBF, 0_usize), input => { let Ok(buff_size) = input.parse::() else { - eprintln!("failed to allocate a {value} byte stdio buffer"); + let _ = writeln!(stderr(), "failed to allocate a {value} byte stdio buffer"); std::process::exit(1); }; (_IOFBF, buff_size as size_t) @@ -223,9 +223,11 @@ fn set_buffer(stream: *mut FILE, value: &str) { res = libc::setvbuf(stream, buffer, mode, size); } if res != 0 { - eprintln!("could not set buffering of {} to mode {mode}", unsafe { - fileno(stream) - }); + let _ = writeln!( + stderr(), + "could not set buffering of {} to mode {mode}", + unsafe { fileno(stream) } + ); } } From 89cbec0cc2b771d5a4d1a8a8c722b6c50a1569eb Mon Sep 17 00:00:00 2001 From: Daniel Tang Date: Mon, 16 Mar 2026 13:32:18 -0400 Subject: [PATCH 192/264] perf(uucore): call rt_sigaction once not 62 times `./coreutils true` becomes 6% faster. For some unknown reason, `capture_startup_state` runs 62 times, so I'll just hotfix it to immediately return from the second time onwards. When 2d7a3bf2e037bac1283c67879d7a9759b2038a6e introduced the fcntl calls, each group of 3 calls was repeated 4 times. Some time later, it became 62 times. Now it runs only 1 time. --- src/uucore/src/lib/features/signals.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/uucore/src/lib/features/signals.rs b/src/uucore/src/lib/features/signals.rs index a5141f299e3..275f7acd266 100644 --- a/src/uucore/src/lib/features/signals.rs +++ b/src/uucore/src/lib/features/signals.rs @@ -538,6 +538,9 @@ static STDERR_WAS_CLOSED: AtomicBool = AtomicBool::new(false); #[cfg(unix)] static SIGPIPE_WAS_IGNORED: AtomicBool = AtomicBool::new(false); +#[cfg(unix)] +static STARTUP_STATE_WAS_CAPTURED: AtomicBool = AtomicBool::new(false); + /// Captures stdio and SIGPIPE state at process initialization, before main() runs. /// /// # Safety @@ -549,6 +552,11 @@ pub unsafe extern "C" fn capture_startup_state() { use std::mem::MaybeUninit; use std::ptr; + // No spinlock because we're single-threaded at this point + if STARTUP_STATE_WAS_CAPTURED.swap(true, Ordering::Relaxed) { + return; + } + // Capture stdio state unsafe { STDIN_WAS_CLOSED.store( From 2512afd2bd16fab87cc7abb48cb7b37b01d8955d Mon Sep 17 00:00:00 2001 From: Michel Lind Date: Mon, 16 Mar 2026 22:21:14 +0000 Subject: [PATCH 193/264] uutests: add LICENSE Symlink LICENSE from the project root so it gets picked up when packaging ``` coreutils/tests/uutests on uutests-add-license $ cargo package --allow-dirty --list | grep LICENSE coreutils/tests/uutests on uutests-add-license $ ln -s ../../LICENSE . coreutils/tests/uutests on uutests-add-license $ cargo package --allow-dirty --list | grep LICENSE LICENSE ``` Signed-off-by: Michel Lind --- tests/uutests/LICENSE | 1 + 1 file changed, 1 insertion(+) create mode 120000 tests/uutests/LICENSE diff --git a/tests/uutests/LICENSE b/tests/uutests/LICENSE new file mode 120000 index 00000000000..30cff7403da --- /dev/null +++ b/tests/uutests/LICENSE @@ -0,0 +1 @@ +../../LICENSE \ No newline at end of file From f12b1a6b59a43267eabb3fc1d66c24ae5b33c31e Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Sat, 4 Apr 2026 18:31:46 +0900 Subject: [PATCH 194/264] uucore: add missing fs feature to rustix --- src/uucore/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uucore/Cargo.toml b/src/uucore/Cargo.toml index 3f1c9af89ef..bd4acb8b2fa 100644 --- a/src/uucore/Cargo.toml +++ b/src/uucore/Cargo.toml @@ -37,7 +37,7 @@ jiff = { workspace = true, optional = true, features = [ "tzdb-concatenated", ] } rustc-hash = { workspace = true } -rustix = { workspace = true, features = ["pipe"] } +rustix = { workspace = true, features = ["fs", "pipe"] } time = { workspace = true, optional = true, features = [ "formatting", "local-offset", From e41e5de37d29d55081bc1d4c013f091c98cec5a9 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Tue, 24 Mar 2026 18:25:26 +0900 Subject: [PATCH 195/264] tee: increase buf size for large input --- src/uu/tee/src/tee.rs | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/src/uu/tee/src/tee.rs b/src/uu/tee/src/tee.rs index dd584b571f8..d7ef5c8d820 100644 --- a/src/uu/tee/src/tee.rs +++ b/src/uu/tee/src/tee.rs @@ -14,7 +14,7 @@ use uucore::format_usage; use uucore::parser::shortcut_value_parser::ShortcutValueParser; use uucore::translate; -// spell-checker:ignore nopipe +// spell-checker:ignore espidf nopipe #[cfg(target_os = "linux")] use uucore::signals::ensure_stdout_not_broken; @@ -209,18 +209,33 @@ fn copy(mut input: impl Read, mut output: impl Write) -> Result { // the standard library: // https://github.com/rust-lang/rust/blob/2feb91181882e525e698c4543063f4d0296fcf91/library/std/src/io/copy.rs#L271-L297 - // Use buffer size from std implementation: + // Use small buffer size from std implementation for small input // https://github.com/rust-lang/rust/blob/2feb91181882e525e698c4543063f4d0296fcf91/library/std/src/sys/io/mod.rs#L44 - // spell-checker:ignore espidf - const DEFAULT_BUF_SIZE: usize = if cfg!(target_os = "espidf") { + const FIRST_BUF_SIZE: usize = if cfg!(target_os = "espidf") { 512 } else { 8 * 1024 }; - - let mut buffer = [0u8; DEFAULT_BUF_SIZE]; + let mut buffer = [0u8; FIRST_BUF_SIZE]; let mut len = 0; + match input.read(&mut buffer) { + Ok(0) => return Ok(0), + Ok(bytes_count) => { + output.write_all(&buffer[0..bytes_count])?; + len = bytes_count; + if bytes_count < FIRST_BUF_SIZE { + // flush the buffer to comply with POSIX requirement that + // `tee` does not buffer the input. + output.flush()?; + return Ok(len); + } + } + Err(e) if e.kind() == ErrorKind::Interrupted => (), + Err(e) => return Err(e), + } + // but optimize buffer size also for large file + let mut buffer = vec![0u8; 4 * FIRST_BUF_SIZE]; //stack array makes code path for smaller file slower loop { match input.read(&mut buffer) { Ok(0) => return Ok(len), // end of file From 787b10be12e582bdbbc6724f6d62853d830113c2 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Sat, 4 Apr 2026 20:06:30 +0900 Subject: [PATCH 196/264] protect names against binary name (#11378) Co-authored-by: Sylvestre Ledru --- src/uu/arch/src/arch.rs | 4 ++-- src/uu/basename/src/basename.rs | 4 ++-- src/uu/cat/src/cat.rs | 8 ++++---- src/uu/chcon/src/chcon.rs | 4 ++-- src/uu/chgrp/src/chgrp.rs | 2 +- src/uu/chmod/src/chmod.rs | 4 ++-- src/uu/chown/src/chown.rs | 4 ++-- src/uu/chroot/src/chroot.rs | 2 +- src/uu/comm/src/comm.rs | 4 ++-- src/uu/cp/src/cp.rs | 4 ++-- src/uu/csplit/src/csplit.rs | 2 +- src/uu/cut/src/cut.rs | 2 +- src/uu/date/src/date.rs | 2 +- src/uu/dd/src/dd.rs | 2 +- src/uu/df/src/df.rs | 6 +++--- src/uu/dircolors/src/dircolors.rs | 4 ++-- src/uu/dirname/src/dirname.rs | 2 +- src/uu/du/src/du.rs | 2 +- src/uu/expand/src/expand.rs | 2 +- src/uu/expr/src/expr.rs | 11 +++-------- src/uu/factor/src/factor.rs | 2 +- src/uu/fmt/src/fmt.rs | 4 ++-- src/uu/fold/src/fold.rs | 2 +- src/uu/groups/src/groups.rs | 4 ++-- src/uu/head/src/head.rs | 4 ++-- src/uu/hostid/src/hostid.rs | 4 ++-- src/uu/hostname/src/hostname.rs | 2 +- src/uu/id/src/id.rs | 4 ++-- src/uu/install/src/install.rs | 6 +++--- src/uu/join/src/join.rs | 2 +- src/uu/kill/src/kill.rs | 4 ++-- src/uu/link/src/link.rs | 4 ++-- src/uu/ln/src/ln.rs | 4 ++-- src/uu/logname/src/logname.rs | 4 ++-- src/uu/mkdir/src/mkdir.rs | 8 ++++---- src/uu/mkfifo/src/mkfifo.rs | 4 ++-- src/uu/mknod/src/mknod.rs | 10 ++++++---- src/uu/mktemp/src/mktemp.rs | 4 ++-- src/uu/more/src/more.rs | 4 ++-- src/uu/mv/src/mv.rs | 2 +- src/uu/nl/src/nl.rs | 2 +- src/uu/nohup/src/nohup.rs | 4 ++-- src/uu/nproc/src/nproc.rs | 4 ++-- src/uu/od/src/od.rs | 4 ++-- src/uu/paste/src/paste.rs | 4 ++-- src/uu/pathchk/src/pathchk.rs | 4 ++-- src/uu/pinky/src/pinky.rs | 2 +- src/uu/pr/src/pr.rs | 4 ++-- src/uu/printenv/src/printenv.rs | 2 +- src/uu/printf/src/printf.rs | 2 +- src/uu/ptx/src/ptx.rs | 4 ++-- src/uu/pwd/src/pwd.rs | 4 ++-- src/uu/readlink/src/readlink.rs | 4 ++-- src/uu/realpath/src/realpath.rs | 4 ++-- src/uu/rm/src/rm.rs | 2 +- src/uu/rmdir/src/rmdir.rs | 8 ++++---- src/uu/runcon/src/runcon.rs | 2 +- src/uu/seq/src/seq.rs | 2 +- src/uu/shred/src/shred.rs | 4 ++-- src/uu/shuf/src/shuf.rs | 4 ++-- src/uu/sleep/src/sleep.rs | 4 ++-- src/uu/sort/src/sort.rs | 2 +- src/uu/split/src/split.rs | 2 +- src/uu/stat/src/stat.rs | 4 ++-- src/uu/stdbuf/src/stdbuf.rs | 4 ++-- src/uu/stty/src/stty.rs | 4 ++-- src/uu/sync/src/sync.rs | 4 ++-- src/uu/tac/src/tac.rs | 4 ++-- src/uu/tail/src/args.rs | 4 ++-- src/uu/tee/src/tee.rs | 4 ++-- src/uu/test/src/test.rs | 7 +++++-- src/uu/timeout/src/timeout.rs | 2 +- src/uu/touch/src/touch.rs | 4 ++-- src/uu/tr/src/tr.rs | 4 ++-- src/uu/truncate/src/truncate.rs | 2 +- src/uu/tsort/src/tsort.rs | 6 +++--- src/uu/tty/src/tty.rs | 2 +- src/uu/uname/src/uname.rs | 4 ++-- src/uu/unexpand/src/unexpand.rs | 2 +- src/uu/uniq/src/uniq.rs | 4 ++-- src/uu/unlink/src/unlink.rs | 4 ++-- src/uu/uptime/src/uptime.rs | 4 ++-- src/uu/users/src/users.rs | 4 ++-- src/uu/wc/src/wc.rs | 2 +- src/uu/who/src/who.rs | 2 +- src/uu/whoami/src/whoami.rs | 4 ++-- src/uu/yes/src/yes.rs | 4 ++-- 87 files changed, 162 insertions(+), 162 deletions(-) diff --git a/src/uu/arch/src/arch.rs b/src/uu/arch/src/arch.rs index f07eb227c41..d0a197ece22 100644 --- a/src/uu/arch/src/arch.rs +++ b/src/uu/arch/src/arch.rs @@ -21,9 +21,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("arch") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("arch")) .about(translate!("arch-about")) .after_help(translate!("arch-after-help")) .override_usage(translate!("arch-usage")) diff --git a/src/uu/basename/src/basename.rs b/src/uu/basename/src/basename.rs index 3ec5cb7c5b3..ee0c90ae0b8 100644 --- a/src/uu/basename/src/basename.rs +++ b/src/uu/basename/src/basename.rs @@ -80,9 +80,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("basename") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("basename")) .about(translate!("basename-about")) .override_usage(format_usage(&translate!("basename-usage"))) .infer_long_args(true) diff --git a/src/uu/cat/src/cat.rs b/src/uu/cat/src/cat.rs index 46e4e1be911..e77c12babac 100644 --- a/src/uu/cat/src/cat.rs +++ b/src/uu/cat/src/cat.rs @@ -268,11 +268,11 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("cat") .version(uucore::crate_version!()) .override_usage(format_usage(&translate!("cat-usage"))) .about(translate!("cat-about")) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("cat")) .infer_long_args(true) .args_override_self(true) .arg( @@ -420,11 +420,11 @@ where Ok(()) } else { // each next line is expected to display "cat: …" - let line_joiner = format!("\n{}: ", uucore::util_name()); + let line_joiner = "\ncat: "; Err(uucore::error::USimpleError::new( error_messages.len() as i32, - error_messages.join(&line_joiner), + error_messages.join(line_joiner), )) } } diff --git a/src/uu/chcon/src/chcon.rs b/src/uu/chcon/src/chcon.rs index 685ad0dcf0d..9953c064fd3 100644 --- a/src/uu/chcon/src/chcon.rs +++ b/src/uu/chcon/src/chcon.rs @@ -156,7 +156,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - let cmd = Command::new(uucore::util_name()) + let cmd = Command::new("chcon") .version(uucore::crate_version!()) .about(translate!("chcon-about")) .override_usage(format_usage(&translate!("chcon-usage"))) @@ -613,7 +613,7 @@ fn process_file( if options.verbose { println!( "{}", - translate!("chcon-verbose-changing-context", "util_name" => uucore::util_name(), "file" => file_full_name.quote()) + translate!("chcon-verbose-changing-context", "util_name" => "chcon", "file" => file_full_name.quote()) ); } diff --git a/src/uu/chgrp/src/chgrp.rs b/src/uu/chgrp/src/chgrp.rs index cd890e49a6f..0e41a67601b 100644 --- a/src/uu/chgrp/src/chgrp.rs +++ b/src/uu/chgrp/src/chgrp.rs @@ -98,7 +98,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - let cmd = Command::new(uucore::util_name()) + let cmd = Command::new("chgrp") .version(uucore::crate_version!()) .about(translate!("chgrp-about")) .override_usage(format_usage(&translate!("chgrp-usage"))) diff --git a/src/uu/chmod/src/chmod.rs b/src/uu/chmod/src/chmod.rs index bb46f513984..a50f0a6bf0e 100644 --- a/src/uu/chmod/src/chmod.rs +++ b/src/uu/chmod/src/chmod.rs @@ -174,11 +174,11 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("chmod") .version(uucore::crate_version!()) .about(translate!("chmod-about")) .override_usage(format_usage(&translate!("chmod-usage"))) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("chmod")) .args_override_self(true) .infer_long_args(true) .no_binary_name(true) diff --git a/src/uu/chown/src/chown.rs b/src/uu/chown/src/chown.rs index 23372742817..ac71abd3804 100644 --- a/src/uu/chown/src/chown.rs +++ b/src/uu/chown/src/chown.rs @@ -76,9 +76,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("chown") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("chown")) .about(translate!("chown-about")) .override_usage(format_usage(&translate!("chown-usage"))) .infer_long_args(true) diff --git a/src/uu/chroot/src/chroot.rs b/src/uu/chroot/src/chroot.rs index a08e4b0cc9a..68e8fe2ed05 100644 --- a/src/uu/chroot/src/chroot.rs +++ b/src/uu/chroot/src/chroot.rs @@ -220,7 +220,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - let cmd = Command::new(uucore::util_name()) + let cmd = Command::new("chroot") .version(uucore::crate_version!()) .about(translate!("chroot-about")) .override_usage(format_usage(&translate!("chroot-usage"))) diff --git a/src/uu/comm/src/comm.rs b/src/uu/comm/src/comm.rs index 9257c1b7d1a..395c561e10e 100644 --- a/src/uu/comm/src/comm.rs +++ b/src/uu/comm/src/comm.rs @@ -374,9 +374,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("comm") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("comm")) .about(translate!("comm-about")) .override_usage(format_usage(&translate!("comm-usage"))) .infer_long_args(true) diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index c769feceea9..c3ca44e7696 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -521,7 +521,7 @@ pub fn uu_app() -> Command { options::ATTRIBUTES_ONLY, options::COPY_CONTENTS, ]; - Command::new(uucore::util_name()) + Command::new("cp") .version(uucore::crate_version!()) .about(translate!("cp-about")) .help_template(uucore::localized_help_template(uucore::util_name())) @@ -1404,7 +1404,7 @@ pub fn copy(sources: &[PathBuf], target: &Path, options: &Options) -> CopyResult ) .unwrap(), ) - .with_message(uucore::util_name()); + .with_message("cp"); pb.tick(); Some(pb) } else { diff --git a/src/uu/csplit/src/csplit.rs b/src/uu/csplit/src/csplit.rs index 4445eb75668..3a4b43e881e 100644 --- a/src/uu/csplit/src/csplit.rs +++ b/src/uu/csplit/src/csplit.rs @@ -645,7 +645,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("csplit") .version(uucore::crate_version!()) .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("csplit-about")) diff --git a/src/uu/cut/src/cut.rs b/src/uu/cut/src/cut.rs index e0568e810e4..1e0cf90bcd8 100644 --- a/src/uu/cut/src/cut.rs +++ b/src/uu/cut/src/cut.rs @@ -680,7 +680,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("cut") .version(uucore::crate_version!()) .help_template(uucore::localized_help_template(uucore::util_name())) .override_usage(format_usage(&translate!("cut-usage"))) diff --git a/src/uu/date/src/date.rs b/src/uu/date/src/date.rs index b82b620a415..dc393f46ffb 100644 --- a/src/uu/date/src/date.rs +++ b/src/uu/date/src/date.rs @@ -591,7 +591,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("date") .version(uucore::crate_version!()) .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("date-about")) diff --git a/src/uu/dd/src/dd.rs b/src/uu/dd/src/dd.rs index bd232a6a0cd..677fe0908ac 100644 --- a/src/uu/dd/src/dd.rs +++ b/src/uu/dd/src/dd.rs @@ -1538,7 +1538,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("dd") .version(uucore::crate_version!()) .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("dd-about")) diff --git a/src/uu/df/src/df.rs b/src/uu/df/src/df.rs index 2f97814d02b..f7d5efdaaf3 100644 --- a/src/uu/df/src/df.rs +++ b/src/uu/df/src/df.rs @@ -142,7 +142,7 @@ enum OptionsError { .0.iter() .map(|t| translate!("df-error-filesystem-type-both-selected-and-excluded", "type" => t.quote())) .collect::>() - .join(format!("\n{}: ", uucore::util_name()).as_str()) + .join("\ndf: ") )] FilesystemTypeBothSelectedAndExcluded(Vec), } @@ -451,7 +451,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { if matches.get_flag(OPT_INODES) { println!( "{}", - translate!("df-error-inodes-not-supported-windows", "program" => uucore::util_name()) + translate!("df-error-inodes-not-supported-windows", "program" => "df") ); return Ok(()); } @@ -498,7 +498,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("df") .version(uucore::crate_version!()) .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("df-about")) diff --git a/src/uu/dircolors/src/dircolors.rs b/src/uu/dircolors/src/dircolors.rs index 3eb2593a51a..154d879b671 100644 --- a/src/uu/dircolors/src/dircolors.rs +++ b/src/uu/dircolors/src/dircolors.rs @@ -238,9 +238,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("dircolors") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("dircolors")) .about(translate!("dircolors-about")) .after_help(translate!("dircolors-after-help")) .override_usage(format_usage(&translate!("dircolors-usage"))) diff --git a/src/uu/dirname/src/dirname.rs b/src/uu/dirname/src/dirname.rs index 09bf9928a3a..561ee742b0f 100644 --- a/src/uu/dirname/src/dirname.rs +++ b/src/uu/dirname/src/dirname.rs @@ -143,7 +143,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("dirname") .about(translate!("dirname-about")) .version(uucore::crate_version!()) .help_template(uucore::localized_help_template(uucore::util_name())) diff --git a/src/uu/du/src/du.rs b/src/uu/du/src/du.rs index bb8c4d88561..7ccfdf5a228 100644 --- a/src/uu/du/src/du.rs +++ b/src/uu/du/src/du.rs @@ -1287,7 +1287,7 @@ fn parse_depth(max_depth_str: Option<&str>, summarize: bool) -> UResult Command { - Command::new(uucore::util_name()) + Command::new("du") .version(uucore::crate_version!()) .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("du-about")) diff --git a/src/uu/expand/src/expand.rs b/src/uu/expand/src/expand.rs index 088e8aaa363..4c3b09f64b7 100644 --- a/src/uu/expand/src/expand.rs +++ b/src/uu/expand/src/expand.rs @@ -249,7 +249,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { uucore::clap_localization::configure_localized_command( - Command::new(uucore::util_name()) + Command::new("expand") .version(uucore::crate_version!()) .about(translate!("expand-about")) .override_usage(format_usage(&translate!("expand-usage"))), diff --git a/src/uu/expr/src/expr.rs b/src/uu/expr/src/expr.rs index f2bec49e8d7..9392fcee72c 100644 --- a/src/uu/expr/src/expr.rs +++ b/src/uu/expr/src/expr.rs @@ -71,9 +71,9 @@ impl UError for ExprError { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("expr") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("expr")) .about(translate!("expr-about")) .override_usage(format_usage(&translate!("expr-usage"))) .after_help(translate!("expr-after-help")) @@ -111,12 +111,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { if args.len() == 1 && args[0] == b"--help" { uu_app().print_help()?; } else if args.len() == 1 && args[0] == b"--version" { - writeln!( - stdout(), - "{} {}", - uucore::util_name(), - uucore::crate_version!() - )?; + writeln!(stdout(), "expr {}", uucore::crate_version!())?; } else { // The first argument may be "--" and should be be ignored. let args = if !args.is_empty() && args[0] == b"--" { diff --git a/src/uu/factor/src/factor.rs b/src/uu/factor/src/factor.rs index 5d381d9d393..4d3d82c52b9 100644 --- a/src/uu/factor/src/factor.rs +++ b/src/uu/factor/src/factor.rs @@ -218,7 +218,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("factor") .version(uucore::crate_version!()) .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("factor-about")) diff --git a/src/uu/fmt/src/fmt.rs b/src/uu/fmt/src/fmt.rs index 42f92d6437f..5f34d062519 100644 --- a/src/uu/fmt/src/fmt.rs +++ b/src/uu/fmt/src/fmt.rs @@ -356,9 +356,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("fmt") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("fmt")) .about(translate!("fmt-about")) .override_usage(format_usage(&translate!("fmt-usage"))) .infer_long_args(true) diff --git a/src/uu/fold/src/fold.rs b/src/uu/fold/src/fold.rs index 76ef483bf31..e7b1688579b 100644 --- a/src/uu/fold/src/fold.rs +++ b/src/uu/fold/src/fold.rs @@ -82,7 +82,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("fold") .version(uucore::crate_version!()) .help_template(uucore::localized_help_template(uucore::util_name())) .override_usage(format_usage(&translate!("fold-usage"))) diff --git a/src/uu/groups/src/groups.rs b/src/uu/groups/src/groups.rs index eed278ed7f4..86128fc63b8 100644 --- a/src/uu/groups/src/groups.rs +++ b/src/uu/groups/src/groups.rs @@ -80,9 +80,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("groups") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("groups")) .about(translate!("groups-about")) .override_usage(format_usage(&translate!("groups-usage"))) .infer_long_args(true) diff --git a/src/uu/head/src/head.rs b/src/uu/head/src/head.rs index bd651bc57aa..43ebc47aa6b 100644 --- a/src/uu/head/src/head.rs +++ b/src/uu/head/src/head.rs @@ -67,9 +67,9 @@ impl UError for HeadError { type HeadResult = Result; pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("head") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("head")) .about(translate!("head-about")) .override_usage(format_usage(&translate!("head-usage"))) .infer_long_args(true) diff --git a/src/uu/hostid/src/hostid.rs b/src/uu/hostid/src/hostid.rs index bad5f9a79f2..04074b0bbda 100644 --- a/src/uu/hostid/src/hostid.rs +++ b/src/uu/hostid/src/hostid.rs @@ -32,9 +32,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("hostid") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("hostid")) .about(translate!("hostid-about")) .override_usage(format_usage(&translate!("hostid-usage"))) .infer_long_args(true) diff --git a/src/uu/hostname/src/hostname.rs b/src/uu/hostname/src/hostname.rs index 747f7794894..53f0ac35e93 100644 --- a/src/uu/hostname/src/hostname.rs +++ b/src/uu/hostname/src/hostname.rs @@ -71,7 +71,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("hostname") .version(uucore::crate_version!()) .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("hostname-about")) diff --git a/src/uu/id/src/id.rs b/src/uu/id/src/id.rs index ff58c711647..78b0ad2d390 100644 --- a/src/uu/id/src/id.rs +++ b/src/uu/id/src/id.rs @@ -361,9 +361,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("id") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("id")) .about(translate!("id-about")) .override_usage(format_usage(&translate!("id-usage"))) .infer_long_args(true) diff --git a/src/uu/install/src/install.rs b/src/uu/install/src/install.rs index a683ff9b908..596d9ebf35d 100644 --- a/src/uu/install/src/install.rs +++ b/src/uu/install/src/install.rs @@ -70,7 +70,7 @@ pub struct Behavior { #[derive(Error, Debug)] enum InstallError { - #[error("{}", translate!("install-error-dir-needs-arg", "util_name" => uucore::util_name()))] + #[error("{}", translate!("install-error-dir-needs-arg", "util_name" => "install"))] DirNeedsArg, #[error("{}", translate!("install-error-create-dir-failed", "path" => .0.quote()))] @@ -193,9 +193,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("install") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("install")) .about(translate!("install-about")) .override_usage(format_usage(&translate!("install-usage"))) .infer_long_args(true) diff --git a/src/uu/join/src/join.rs b/src/uu/join/src/join.rs index 1d6801ba47f..3f95be4bc51 100644 --- a/src/uu/join/src/join.rs +++ b/src/uu/join/src/join.rs @@ -863,7 +863,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("join") .version(uucore::crate_version!()) .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("join-about")) diff --git a/src/uu/kill/src/kill.rs b/src/uu/kill/src/kill.rs index 81186b5cb5a..e92a47da3b4 100644 --- a/src/uu/kill/src/kill.rs +++ b/src/uu/kill/src/kill.rs @@ -100,9 +100,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("kill") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("kill")) .about(translate!("kill-about")) .override_usage(format_usage(&translate!("kill-usage"))) .infer_long_args(true) diff --git a/src/uu/link/src/link.rs b/src/uu/link/src/link.rs index 12ebbbee916..7af8dd0905c 100644 --- a/src/uu/link/src/link.rs +++ b/src/uu/link/src/link.rs @@ -34,9 +34,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("link") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("link")) .about(translate!("link-about")) .override_usage(format_usage(&translate!("link-usage"))) .infer_long_args(true) diff --git a/src/uu/ln/src/ln.rs b/src/uu/ln/src/ln.rs index f7c953908a1..5fb75a86730 100644 --- a/src/uu/ln/src/ln.rs +++ b/src/uu/ln/src/ln.rs @@ -143,9 +143,9 @@ pub fn uu_app() -> Command { backup_control::BACKUP_CONTROL_LONG_HELP ); - Command::new(uucore::util_name()) + Command::new("ln") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("ln")) .about(translate!("ln-about")) .override_usage(format_usage(&translate!("ln-usage"))) .infer_long_args(true) diff --git a/src/uu/logname/src/logname.rs b/src/uu/logname/src/logname.rs index 3badf4c1cb4..07ac97be637 100644 --- a/src/uu/logname/src/logname.rs +++ b/src/uu/logname/src/logname.rs @@ -34,9 +34,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("logname") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("logname")) .override_usage(translate!("logname-usage")) .about(translate!("logname-about")) .infer_long_args(true) diff --git a/src/uu/mkdir/src/mkdir.rs b/src/uu/mkdir/src/mkdir.rs index 3d0a9bc33c9..5db16c1d601 100644 --- a/src/uu/mkdir/src/mkdir.rs +++ b/src/uu/mkdir/src/mkdir.rs @@ -104,9 +104,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("mkdir") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("mkdir")) .about(translate!("mkdir-about")) .override_usage(format_usage(&translate!("mkdir-usage"))) .infer_long_args(true) @@ -316,7 +316,7 @@ fn create_single_dir(path: &Path, is_parent: bool, config: &Config) -> UResult<( writeln!( stdout(), "{}", - translate!("mkdir-verbose-created-directory", "util_name" => uucore::util_name(), "path" => path.quote()) + translate!("mkdir-verbose-created-directory", "util_name" => "mkdir", "path" => path.quote()) )?; } @@ -366,7 +366,7 @@ fn create_single_dir(path: &Path, is_parent: bool, config: &Config) -> UResult<( writeln!( stdout(), "{}", - translate!("mkdir-verbose-created-directory", "util_name" => uucore::util_name(), "path" => path.quote()) + translate!("mkdir-verbose-created-directory", "util_name" => "mkdir", "path" => path.quote()) )?; } Ok(()) diff --git a/src/uu/mkfifo/src/mkfifo.rs b/src/uu/mkfifo/src/mkfifo.rs index a70d140c7c6..a48a4894ee6 100644 --- a/src/uu/mkfifo/src/mkfifo.rs +++ b/src/uu/mkfifo/src/mkfifo.rs @@ -97,9 +97,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("mkfifo") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("mkfifo")) .override_usage(format_usage(&translate!("mkfifo-usage"))) .about(translate!("mkfifo-about")) .infer_long_args(true) diff --git a/src/uu/mknod/src/mknod.rs b/src/uu/mknod/src/mknod.rs index 9b6ab45f648..39ac62cb62d 100644 --- a/src/uu/mknod/src/mknod.rs +++ b/src/uu/mknod/src/mknod.rs @@ -104,7 +104,8 @@ fn mknod(file_name: &str, config: Config) -> i32 { ) { // if it fails, delete the file let _ = std::fs::remove_file(file_name); - eprintln!("{}: {e}", uucore::util_name()); + use std::io::{Write, stderr}; + let _ = writeln!(stderr(), "mknod: {e}"); return 1; } } @@ -117,7 +118,8 @@ fn mknod(file_name: &str, config: Config) -> i32 { std::fs::remove_file(p) }) { - eprintln!("{}: {e}", uucore::util_name()); + use std::io::{Write, stderr}; + let _ = writeln!(stderr(), "mknod: {e}"); return 1; } } @@ -189,9 +191,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("mknod") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("mknod")) .override_usage(format_usage(&translate!("mknod-usage"))) .after_help(translate!("mknod-after-help")) .about(translate!("mknod-about")) diff --git a/src/uu/mktemp/src/mktemp.rs b/src/uu/mktemp/src/mktemp.rs index f45c676bede..f0cd78cfe55 100644 --- a/src/uu/mktemp/src/mktemp.rs +++ b/src/uu/mktemp/src/mktemp.rs @@ -434,9 +434,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("mktemp") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("mktemp")) .about(translate!("mktemp-about")) .override_usage(format_usage(&translate!("mktemp-usage"))) .infer_long_args(true) diff --git a/src/uu/more/src/more.rs b/src/uu/more/src/more.rs index f31521a6e08..0ad3006024c 100644 --- a/src/uu/more/src/more.rs +++ b/src/uu/more/src/more.rs @@ -206,11 +206,11 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("more") .about(translate!("more-about")) .override_usage(format_usage(&translate!("more-usage"))) .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("more")) .infer_long_args(true) .arg( Arg::new(options::SILENT) diff --git a/src/uu/mv/src/mv.rs b/src/uu/mv/src/mv.rs index 28901767e7b..b5f789ff0ba 100644 --- a/src/uu/mv/src/mv.rs +++ b/src/uu/mv/src/mv.rs @@ -226,7 +226,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("mv") .version(uucore::crate_version!()) .about(translate!("mv-about")) .help_template(uucore::localized_help_template(uucore::util_name())) diff --git a/src/uu/nl/src/nl.rs b/src/uu/nl/src/nl.rs index f32bed18b2c..b9d62c4a7e4 100644 --- a/src/uu/nl/src/nl.rs +++ b/src/uu/nl/src/nl.rs @@ -265,7 +265,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("nl") .about(translate!("nl-about")) .version(uucore::crate_version!()) .help_template(uucore::localized_help_template(uucore::util_name())) diff --git a/src/uu/nohup/src/nohup.rs b/src/uu/nohup/src/nohup.rs index 441d3e967d8..ccf14d2e720 100644 --- a/src/uu/nohup/src/nohup.rs +++ b/src/uu/nohup/src/nohup.rs @@ -95,9 +95,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("nohup") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("nohup")) .about(translate!("nohup-about")) .after_help(translate!("nohup-after-help")) .override_usage(format_usage(&translate!("nohup-usage"))) diff --git a/src/uu/nproc/src/nproc.rs b/src/uu/nproc/src/nproc.rs index bc6a8a30007..9edf21bf613 100644 --- a/src/uu/nproc/src/nproc.rs +++ b/src/uu/nproc/src/nproc.rs @@ -86,9 +86,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("nproc") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("nproc")) .about(translate!("nproc-about")) .override_usage(format_usage(&translate!("nproc-usage"))) .infer_long_args(true) diff --git a/src/uu/od/src/od.rs b/src/uu/od/src/od.rs index 67992d76955..1a16b9276dd 100644 --- a/src/uu/od/src/od.rs +++ b/src/uu/od/src/od.rs @@ -290,9 +290,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("od") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("od")) .about(translate!("od-about")) .override_usage(format_usage(&translate!("od-usage"))) .after_help(translate!("od-after-help")) diff --git a/src/uu/paste/src/paste.rs b/src/uu/paste/src/paste.rs index 3d3fc3ffdab..95638c30bb4 100644 --- a/src/uu/paste/src/paste.rs +++ b/src/uu/paste/src/paste.rs @@ -42,9 +42,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("paste") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("paste")) .about(translate!("paste-about")) .override_usage(format_usage(&translate!("paste-usage"))) .infer_long_args(true) diff --git a/src/uu/pathchk/src/pathchk.rs b/src/uu/pathchk/src/pathchk.rs index 2bf7b40a802..0f015c8614f 100644 --- a/src/uu/pathchk/src/pathchk.rs +++ b/src/uu/pathchk/src/pathchk.rs @@ -95,9 +95,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("paste") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("paste")) .about(translate!("pathchk-about")) .override_usage(format_usage(&translate!("pathchk-usage"))) .infer_long_args(true) diff --git a/src/uu/pinky/src/pinky.rs b/src/uu/pinky/src/pinky.rs index 3ae07370a63..a0779799396 100644 --- a/src/uu/pinky/src/pinky.rs +++ b/src/uu/pinky/src/pinky.rs @@ -35,7 +35,7 @@ pub fn uu_app() -> Command { #[cfg(target_env = "musl")] let about = translate!("pinky-about") + &translate!("pinky-about-musl-warning"); - let cmd = Command::new(uucore::util_name()) + let cmd = Command::new("pinky") .version(uucore::crate_version!()) .about(about) .override_usage(format_usage(&translate!("pinky-usage"))) diff --git a/src/uu/pr/src/pr.rs b/src/uu/pr/src/pr.rs index 6048a5978f9..d74c65cc592 100644 --- a/src/uu/pr/src/pr.rs +++ b/src/uu/pr/src/pr.rs @@ -192,9 +192,9 @@ enum PrError { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("pr") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("pr")) .about(translate!("pr-about")) .after_help(translate!("pr-after-help")) .override_usage(format_usage(&translate!("pr-usage"))) diff --git a/src/uu/printenv/src/printenv.rs b/src/uu/printenv/src/printenv.rs index e9f09f1d6e1..0005577c159 100644 --- a/src/uu/printenv/src/printenv.rs +++ b/src/uu/printenv/src/printenv.rs @@ -53,7 +53,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - let cmd = Command::new(uucore::util_name()) + let cmd = Command::new("printenv") .version(uucore::crate_version!()) .about(translate!("printenv-about")) .override_usage(format_usage(&translate!("printenv-usage"))) diff --git a/src/uu/printf/src/printf.rs b/src/uu/printf/src/printf.rs index 69be56c911d..4cf58bbeb5b 100644 --- a/src/uu/printf/src/printf.rs +++ b/src/uu/printf/src/printf.rs @@ -82,7 +82,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("printf") .allow_hyphen_values(true) .version(uucore::crate_version!()) .help_template(uucore::localized_help_template(uucore::util_name())) diff --git a/src/uu/ptx/src/ptx.rs b/src/uu/ptx/src/ptx.rs index 1811a29bdc0..64a5e2bacbe 100644 --- a/src/uu/ptx/src/ptx.rs +++ b/src/uu/ptx/src/ptx.rs @@ -953,10 +953,10 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("ptx") .about(translate!("ptx-about")) .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("ptx")) .override_usage(format_usage(&translate!("ptx-usage"))) .infer_long_args(true) .arg( diff --git a/src/uu/pwd/src/pwd.rs b/src/uu/pwd/src/pwd.rs index b1670b06d0a..193af4ccc5f 100644 --- a/src/uu/pwd/src/pwd.rs +++ b/src/uu/pwd/src/pwd.rs @@ -139,9 +139,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("pwd") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("pwd")) .about(translate!("pwd-about")) .override_usage(format_usage(&translate!("pwd-usage"))) .infer_long_args(true) diff --git a/src/uu/readlink/src/readlink.rs b/src/uu/readlink/src/readlink.rs index 4fc8eaa0e9c..89499cf388f 100644 --- a/src/uu/readlink/src/readlink.rs +++ b/src/uu/readlink/src/readlink.rs @@ -112,9 +112,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("readlink") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("readlink")) .about(translate!("readlink-about")) .override_usage(format_usage(&translate!("readlink-usage"))) .infer_long_args(true) diff --git a/src/uu/realpath/src/realpath.rs b/src/uu/realpath/src/realpath.rs index 860e1361ec8..f8551e7fc55 100644 --- a/src/uu/realpath/src/realpath.rs +++ b/src/uu/realpath/src/realpath.rs @@ -126,9 +126,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("realpath") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("realpath")) .about(translate!("realpath-about")) .override_usage(format_usage(&translate!("realpath-usage"))) .infer_long_args(true) diff --git a/src/uu/rm/src/rm.rs b/src/uu/rm/src/rm.rs index b1c7d1dedb8..5f2b981ea2b 100644 --- a/src/uu/rm/src/rm.rs +++ b/src/uu/rm/src/rm.rs @@ -294,7 +294,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("rm") .version(uucore::crate_version!()) .about(translate!("rm-about")) .help_template(uucore::localized_help_template(uucore::util_name())) diff --git a/src/uu/rmdir/src/rmdir.rs b/src/uu/rmdir/src/rmdir.rs index e0c9f73bcec..76fa126734a 100644 --- a/src/uu/rmdir/src/rmdir.rs +++ b/src/uu/rmdir/src/rmdir.rs @@ -15,7 +15,7 @@ use uucore::display::Quotable; use uucore::error::{UResult, set_exit_code, strip_errno}; use uucore::translate; -use uucore::{format_usage, show_error, util_name}; +use uucore::{format_usage, show_error}; static OPT_IGNORE_FAIL_NON_EMPTY: &str = "ignore-fail-on-non-empty"; static OPT_PARENTS: &str = "parents"; @@ -114,7 +114,7 @@ fn remove_single(path: &Path, opts: Opts) -> Result<(), Error<'_>> { if opts.verbose { println!( "{}", - translate!("rmdir-verbose-removing-directory", "util_name" => util_name(), "path" => path.quote()) + translate!("rmdir-verbose-removing-directory", "util_name" => "rmdir", "path" => path.quote()) ); } remove_dir(path).map_err(|error| Error { error, path }) @@ -178,9 +178,9 @@ struct Opts { } pub fn uu_app() -> Command { - Command::new(util_name()) + Command::new("rmdir") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(util_name())) + .help_template(uucore::localized_help_template("rmdir")) .about(translate!("rmdir-about")) .override_usage(format_usage(&translate!("rmdir-usage"))) .infer_long_args(true) diff --git a/src/uu/runcon/src/runcon.rs b/src/uu/runcon/src/runcon.rs index 128d0dce370..1f6ab00dbd6 100644 --- a/src/uu/runcon/src/runcon.rs +++ b/src/uu/runcon/src/runcon.rs @@ -85,7 +85,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - let cmd = Command::new(uucore::util_name()) + let cmd = Command::new("runcon") .version(uucore::crate_version!()) .about(translate!("runcon-about")) .after_help(translate!("runcon-after-help")) diff --git a/src/uu/seq/src/seq.rs b/src/uu/seq/src/seq.rs index 0f32cf01f7c..3860f923bb4 100644 --- a/src/uu/seq/src/seq.rs +++ b/src/uu/seq/src/seq.rs @@ -228,7 +228,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("seq") .trailing_var_arg(true) .infer_long_args(true) .version(uucore::crate_version!()) diff --git a/src/uu/shred/src/shred.rs b/src/uu/shred/src/shred.rs index 7d19a42fc58..f35b1a0db2c 100644 --- a/src/uu/shred/src/shred.rs +++ b/src/uu/shred/src/shred.rs @@ -318,9 +318,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("shred") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("shred")) .about(translate!("shred-about")) .after_help(translate!("shred-after-help")) .override_usage(format_usage(&translate!("shred-usage"))) diff --git a/src/uu/shuf/src/shuf.rs b/src/uu/shuf/src/shuf.rs index de725cc4e20..d3dd00a1905 100644 --- a/src/uu/shuf/src/shuf.rs +++ b/src/uu/shuf/src/shuf.rs @@ -172,10 +172,10 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("shuf") .about(translate!("shuf-about")) .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("shuf")) .override_usage(format_usage(&translate!("shuf-usage"))) .infer_long_args(true) .arg( diff --git a/src/uu/sleep/src/sleep.rs b/src/uu/sleep/src/sleep.rs index 5b1a1f510a0..cd82aa59185 100644 --- a/src/uu/sleep/src/sleep.rs +++ b/src/uu/sleep/src/sleep.rs @@ -37,9 +37,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("sleep") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("sleep")) .about(translate!("sleep-about")) .after_help(translate!("sleep-after-help")) .override_usage(format_usage(&translate!("sleep-usage"))) diff --git a/src/uu/sort/src/sort.rs b/src/uu/sort/src/sort.rs index 02822cee56d..8b321b55d53 100644 --- a/src/uu/sort/src/sort.rs +++ b/src/uu/sort/src/sort.rs @@ -2356,7 +2356,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { uucore::clap_localization::configure_localized_command( - Command::new(uucore::util_name()) + Command::new("sort") .version(uucore::crate_version!()) .about(translate!("sort-about")) .after_help(translate!("sort-after-help")) diff --git a/src/uu/split/src/split.rs b/src/uu/split/src/split.rs index 31b1ec3c9ea..1540950c172 100644 --- a/src/uu/split/src/split.rs +++ b/src/uu/split/src/split.rs @@ -234,7 +234,7 @@ fn handle_preceding_options( } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("split") .version(uucore::crate_version!()) .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("split-about")) diff --git a/src/uu/stat/src/stat.rs b/src/uu/stat/src/stat.rs index e91e0f3eb93..468f9ed6c23 100644 --- a/src/uu/stat/src/stat.rs +++ b/src/uu/stat/src/stat.rs @@ -1353,9 +1353,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("stat") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("stat")) .about(translate!("stat-about")) .after_help(translate!("stat-after-help")) .override_usage(format_usage(&translate!("stat-usage"))) diff --git a/src/uu/stdbuf/src/stdbuf.rs b/src/uu/stdbuf/src/stdbuf.rs index f8927967d8b..0bf64922bdc 100644 --- a/src/uu/stdbuf/src/stdbuf.rs +++ b/src/uu/stdbuf/src/stdbuf.rs @@ -230,9 +230,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("stdbuf") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("stdbuf")) .about(translate!("stdbuf-about")) .after_help(translate!("stdbuf-after-help")) .override_usage(format_usage(&translate!("stdbuf-usage"))) diff --git a/src/uu/stty/src/stty.rs b/src/uu/stty/src/stty.rs index 6909d2c4f5f..686f00a823e 100644 --- a/src/uu/stty/src/stty.rs +++ b/src/uu/stty/src/stty.rs @@ -1253,9 +1253,9 @@ fn get_sane_control_char(cc_index: S) -> u8 { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("stty") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("stty")) .override_usage(format_usage(&translate!("stty-usage"))) .about(translate!("stty-about")) .infer_long_args(true) diff --git a/src/uu/sync/src/sync.rs b/src/uu/sync/src/sync.rs index 8e81fd03015..b34d0ac0bd1 100644 --- a/src/uu/sync/src/sync.rs +++ b/src/uu/sync/src/sync.rs @@ -279,9 +279,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("sync") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("sync")) .about(translate!("sync-about")) .override_usage(format_usage(&translate!("sync-usage"))) .infer_long_args(true) diff --git a/src/uu/tac/src/tac.rs b/src/uu/tac/src/tac.rs index 88c355b0e4b..50e615c988b 100644 --- a/src/uu/tac/src/tac.rs +++ b/src/uu/tac/src/tac.rs @@ -56,9 +56,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("tac") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("tac")) .override_usage(format_usage(&translate!("tac-usage"))) .about(translate!("tac-about")) .infer_long_args(true) diff --git a/src/uu/tail/src/args.rs b/src/uu/tail/src/args.rs index c1af12eddac..155f7cc1d53 100644 --- a/src/uu/tail/src/args.rs +++ b/src/uu/tail/src/args.rs @@ -450,9 +450,9 @@ pub fn uu_app() -> Command { #[cfg(not(any(unix, target_os = "windows")))] let polling_help = translate!("tail-help-polling-unix"); - Command::new(uucore::util_name()) + Command::new("tail") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("tail")) .about(translate!("tail-about")) .override_usage(format_usage(&translate!("tail-usage"))) .infer_long_args(true) diff --git a/src/uu/tee/src/tee.rs b/src/uu/tee/src/tee.rs index d7ef5c8d820..da4b921861e 100644 --- a/src/uu/tee/src/tee.rs +++ b/src/uu/tee/src/tee.rs @@ -85,9 +85,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("tee") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("tee")) .about(translate!("tee-about")) .override_usage(format_usage(&translate!("tee-usage"))) .after_help(translate!("tee-after-help")) diff --git a/src/uu/test/src/test.rs b/src/uu/test/src/test.rs index 92b1dcdde02..38c0072e15e 100644 --- a/src/uu/test/src/test.rs +++ b/src/uu/test/src/test.rs @@ -33,7 +33,8 @@ use uucore::translate; pub fn uu_app() -> Command { // Disable printing of -h and -v as valid alternatives for --help and --version, // since we don't recognize -h and -v as help/version flags. - Command::new(uucore::util_name()) + // We change the name to test later + Command::new("[") .version(uucore::crate_version!()) .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("test-about")) @@ -64,8 +65,10 @@ pub fn uumain(mut args: impl uucore::Args) -> UResult<()> { translate!("test-error-missing-closing-bracket"), )); } + } else { + // Show actual name with error + let _ = uu_app().name("test"); } - let result = parse(args).map(|mut stack| eval(&mut stack))??; if result { Ok(()) } else { Err(1.into()) } diff --git a/src/uu/timeout/src/timeout.rs b/src/uu/timeout/src/timeout.rs index 5d5511cf1ed..25590ebd611 100644 --- a/src/uu/timeout/src/timeout.rs +++ b/src/uu/timeout/src/timeout.rs @@ -125,7 +125,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new("timeout") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("timeout")) .about(translate!("timeout-about")) .override_usage(format_usage(&translate!("timeout-usage"))) .arg( diff --git a/src/uu/touch/src/touch.rs b/src/uu/touch/src/touch.rs index 4cf4dce2299..beeb738a241 100644 --- a/src/uu/touch/src/touch.rs +++ b/src/uu/touch/src/touch.rs @@ -263,9 +263,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("touch") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("touch")) .about(translate!("touch-about")) .override_usage(format_usage(&translate!("touch-usage"))) .infer_long_args(true) diff --git a/src/uu/tr/src/tr.rs b/src/uu/tr/src/tr.rs index 33f61d17fe6..58976d3ac06 100644 --- a/src/uu/tr/src/tr.rs +++ b/src/uu/tr/src/tr.rs @@ -148,9 +148,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("tr") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("tr")) .about(translate!("tr-about")) .override_usage(format_usage(&translate!("tr-usage"))) .after_help(translate!("tr-after-help")) diff --git a/src/uu/truncate/src/truncate.rs b/src/uu/truncate/src/truncate.rs index 6f460cfd708..31a67f5f82a 100644 --- a/src/uu/truncate/src/truncate.rs +++ b/src/uu/truncate/src/truncate.rs @@ -124,7 +124,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - let cmd = Command::new(uucore::util_name()) + let cmd = Command::new("truncate") .version(uucore::crate_version!()) .about(translate!("truncate-about")) .override_usage(format_usage(&translate!("truncate-usage"))) diff --git a/src/uu/tsort/src/tsort.rs b/src/uu/tsort/src/tsort.rs index 1868af85c3a..c6a6829d67b 100644 --- a/src/uu/tsort/src/tsort.rs +++ b/src/uu/tsort/src/tsort.rs @@ -49,7 +49,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { translate!( "tsort-error-extra-operand", "operand" => extra.quote(), - "util" => uucore::util_name() + "util" => "tsort" ), )); } @@ -107,9 +107,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("tsort") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("tsort")) .override_usage(format_usage(&translate!("tsort-usage"))) .about(translate!("tsort-about")) .infer_long_args(true) diff --git a/src/uu/tty/src/tty.rs b/src/uu/tty/src/tty.rs index 3dfef73225e..edab189fd76 100644 --- a/src/uu/tty/src/tty.rs +++ b/src/uu/tty/src/tty.rs @@ -60,7 +60,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - let cmd = Command::new(uucore::util_name()) + let cmd = Command::new("tty") .version(uucore::crate_version!()) .about(translate!("tty-about")) .override_usage(format_usage(&translate!("tty-usage"))) diff --git a/src/uu/uname/src/uname.rs b/src/uu/uname/src/uname.rs index f83ef0d12b3..1439bd6f7f2 100644 --- a/src/uu/uname/src/uname.rs +++ b/src/uu/uname/src/uname.rs @@ -141,9 +141,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("uname") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("uname")) .about(translate!("uname-about")) .override_usage(format_usage(&translate!("uname-usage"))) .infer_long_args(true) diff --git a/src/uu/unexpand/src/unexpand.rs b/src/uu/unexpand/src/unexpand.rs index eae91164608..9c963f7aef3 100644 --- a/src/uu/unexpand/src/unexpand.rs +++ b/src/uu/unexpand/src/unexpand.rs @@ -235,7 +235,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("unexpand") .version(uucore::crate_version!()) .help_template(uucore::localized_help_template(uucore::util_name())) .override_usage(format_usage(&translate!("unexpand-usage"))) diff --git a/src/uu/uniq/src/uniq.rs b/src/uu/uniq/src/uniq.rs index 109e49d6224..e249aabe63c 100644 --- a/src/uu/uniq/src/uniq.rs +++ b/src/uu/uniq/src/uniq.rs @@ -655,7 +655,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { return Err(map_clap_errors(clap_error)); } // Use ErrorFormatter directly to handle error - let formatter = uucore::clap_localization::ErrorFormatter::new(uucore::util_name()); + let formatter = uucore::clap_localization::ErrorFormatter::new("uniq"); formatter.print_error_and_exit_with_callback(&clap_error, 1, || {}); } }; @@ -699,7 +699,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - let cmd = Command::new(uucore::util_name()) + let cmd = Command::new("uniq") .version(uucore::crate_version!()) .about(translate!("uniq-about")) .override_usage(format_usage(&translate!("uniq-usage"))) diff --git a/src/uu/unlink/src/unlink.rs b/src/uu/unlink/src/unlink.rs index caea413da51..188b6ad2d7f 100644 --- a/src/uu/unlink/src/unlink.rs +++ b/src/uu/unlink/src/unlink.rs @@ -27,9 +27,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("unlink") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("unlink")) .about(translate!("unlink-about")) .override_usage(format_usage(&translate!("unlink-usage"))) .infer_long_args(true) diff --git a/src/uu/uptime/src/uptime.rs b/src/uu/uptime/src/uptime.rs index 71ffd980d66..36c8760fd70 100644 --- a/src/uu/uptime/src/uptime.rs +++ b/src/uu/uptime/src/uptime.rs @@ -74,9 +74,9 @@ pub fn uu_app() -> Command { #[cfg(target_env = "musl")] let about = translate!("uptime-about") + &translate!("uptime-about-musl-warning"); - let cmd = Command::new(uucore::util_name()) + let cmd = Command::new("uptime") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("uptime")) .about(about) .override_usage(format_usage(&translate!("uptime-usage"))) .infer_long_args(true) diff --git a/src/uu/users/src/users.rs b/src/uu/users/src/users.rs index ea4b63d0d2a..5d78b17f9a7 100644 --- a/src/uu/users/src/users.rs +++ b/src/uu/users/src/users.rs @@ -86,9 +86,9 @@ pub fn uu_app() -> Command { #[cfg(target_env = "musl")] let about = translate!("users-about") + &translate!("users-about-musl-warning"); - Command::new(uucore::util_name()) + Command::new("users") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("users")) .about(about) .override_usage(format_usage(&translate!("users-usage"))) .infer_long_args(true) diff --git a/src/uu/wc/src/wc.rs b/src/uu/wc/src/wc.rs index 5cc1bcd9017..90c80752f74 100644 --- a/src/uu/wc/src/wc.rs +++ b/src/uu/wc/src/wc.rs @@ -392,7 +392,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("wc") .version(uucore::crate_version!()) .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("wc-about")) diff --git a/src/uu/who/src/who.rs b/src/uu/who/src/who.rs index 197fbe71287..e82990b7924 100644 --- a/src/uu/who/src/who.rs +++ b/src/uu/who/src/who.rs @@ -45,7 +45,7 @@ pub fn uu_app() -> Command { #[cfg(target_env = "musl")] let about = translate!("who-about") + &translate!("who-about-musl-warning"); - let cmd = Command::new(uucore::util_name()) + let cmd = Command::new("who") .version(uucore::crate_version!()) .about(about) .override_usage(format_usage(&translate!("who-usage"))) diff --git a/src/uu/whoami/src/whoami.rs b/src/uu/whoami/src/whoami.rs index 3851c10748d..17f50d1a62f 100644 --- a/src/uu/whoami/src/whoami.rs +++ b/src/uu/whoami/src/whoami.rs @@ -25,9 +25,9 @@ pub fn whoami() -> UResult { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("whoami") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("whoami")) .about(translate!("whoami-about")) .override_usage(translate!("whoami-usage")) .infer_long_args(true) diff --git a/src/uu/yes/src/yes.rs b/src/uu/yes/src/yes.rs index c9e86f99e38..e7132959580 100644 --- a/src/uu/yes/src/yes.rs +++ b/src/uu/yes/src/yes.rs @@ -39,9 +39,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("yes") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("yes")) .about(translate!("yes-about")) .override_usage(format_usage(&translate!("yes-usage"))) .arg( From d5e3da265dd6510a6fbc75dcb0f0b5da11e5556b Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Sat, 4 Apr 2026 20:00:31 +0900 Subject: [PATCH 197/264] cat: fallback with pipe() failure --- src/uu/cat/src/splice.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/uu/cat/src/splice.rs b/src/uu/cat/src/splice.rs index c2fceaa9083..87cbff81a3e 100644 --- a/src/uu/cat/src/splice.rs +++ b/src/uu/cat/src/splice.rs @@ -34,9 +34,8 @@ pub(super) fn write_fast_using_splice( Err(_) => return Ok(true), } } - } else { + } else if let Ok((pipe_rd, pipe_wr)) = pipe() { // both of in/output are not pipe. needs broker to use splice() with additional costs - let (pipe_rd, pipe_wr) = pipe()?; loop { match splice(&handle.reader, &pipe_wr, MAX_ROOTLESS_PIPE_SIZE) { Ok(0) => return Ok(false), @@ -54,6 +53,8 @@ pub(super) fn write_fast_using_splice( Err(_) => return Ok(true), } } + } else { + Ok(true) } } From 9ed6511b73f9d4c7165da061f7833472e32b89c7 Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Sat, 4 Apr 2026 21:49:13 +0200 Subject: [PATCH 198/264] date: fix "unused variable" warning (#11637) --- src/uu/date/src/date.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/uu/date/src/date.rs b/src/uu/date/src/date.rs index dc393f46ffb..d9b48da08f3 100644 --- a/src/uu/date/src/date.rs +++ b/src/uu/date/src/date.rs @@ -706,7 +706,8 @@ fn format_date_with_locale_aware_months( date: &Zoned, format_string: &str, config: &Config, - skip_localization: bool, + #[cfg(feature = "i18n-datetime")] skip_localization: bool, + #[cfg(not(feature = "i18n-datetime"))] _skip_localization: bool, ) -> Result { // First check if format string has GNU modifiers (width/flags) and format if present // This optimization combines detection and formatting in a single pass From 38d814531334ca0054b7a7a59988eee4c78dc61e Mon Sep 17 00:00:00 2001 From: mattsu <35655889+mattsu2020@users.noreply.github.com> Date: Sun, 5 Apr 2026 05:02:21 +0900 Subject: [PATCH 199/264] feat(timeout): add benchmarking support with divan (#9733) * deps: bump codspeed-divan-compat to v4.2.1 * bench(timeout): add divan benchmarks * ci(benchmarks): include uu_timeout in codspeed list --------- Co-authored-by: Sylvestre Ledru --- .github/workflows/benchmarks.yml | 1 + Cargo.lock | 1 + src/uu/timeout/Cargo.toml | 8 ++++++ src/uu/timeout/benches/timeout_bench.rs | 34 +++++++++++++++++++++++++ 4 files changed, 44 insertions(+) create mode 100644 src/uu/timeout/benches/timeout_bench.rs diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 17c37e41a93..9c8c180bf45 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -48,6 +48,7 @@ jobs: uu_shuf, uu_sort, uu_split, + uu_timeout, uu_tsort, uu_unexpand, uu_uniq, diff --git a/Cargo.lock b/Cargo.lock index 4cff737eed3..1bfb8a47a37 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4287,6 +4287,7 @@ name = "uu_timeout" version = "0.8.0" dependencies = [ "clap", + "codspeed-divan-compat", "fluent", "libc", "nix", diff --git a/src/uu/timeout/Cargo.toml b/src/uu/timeout/Cargo.toml index 90a0b378c4a..ecc10470195 100644 --- a/src/uu/timeout/Cargo.toml +++ b/src/uu/timeout/Cargo.toml @@ -30,3 +30,11 @@ nix = { workspace = true, features = ["signal"] } [[bin]] name = "timeout" path = "src/main.rs" + +[dev-dependencies] +divan = { workspace = true } +uucore = { workspace = true, features = ["benchmark"] } + +[[bench]] +name = "timeout_bench" +harness = false diff --git a/src/uu/timeout/benches/timeout_bench.rs b/src/uu/timeout/benches/timeout_bench.rs new file mode 100644 index 00000000000..511955f6f0b --- /dev/null +++ b/src/uu/timeout/benches/timeout_bench.rs @@ -0,0 +1,34 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +#[cfg(unix)] +use divan::{Bencher, black_box}; +#[cfg(unix)] +use uu_timeout::uumain; +#[cfg(unix)] +use uucore::benchmark::run_util_function; + +/// Benchmark the fast path where the command exits immediately. +#[cfg(unix)] +#[divan::bench] +fn timeout_quick_exit(bencher: Bencher) { + bencher.bench(|| { + black_box(run_util_function(uumain, &["0.02", "true"])); + }); +} + +/// Benchmark a command that runs longer than the threshold and receives the default signal. +#[cfg(unix)] +#[divan::bench] +fn timeout_enforced(bencher: Bencher) { + bencher.bench(|| { + black_box(run_util_function(uumain, &["0.02", "sleep", "0.2"])); + }); +} + +fn main() { + #[cfg(unix)] + divan::main(); +} From d8f24b575437a6ceddc917927d54be21d5cb967f Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sat, 4 Apr 2026 22:21:07 +0200 Subject: [PATCH 200/264] ls: fall back to "." metadata for ".." on WASI (#11633) * ls: fall back to "." metadata for ".." on WASI On WASI the sandbox blocks access to the parent directory at the preopened root, causing ls -a to show an error and display ".." with question marks. Fall back to the current directory metadata so the entry appears with valid information. --- .vscode/cspell.dictionaries/jargon.wordlist.txt | 3 +++ src/uu/ls/src/ls.rs | 14 +++++++++++++- tests/by-util/test_ls.rs | 15 +++++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/.vscode/cspell.dictionaries/jargon.wordlist.txt b/.vscode/cspell.dictionaries/jargon.wordlist.txt index e4c1592b475..16778da940d 100644 --- a/.vscode/cspell.dictionaries/jargon.wordlist.txt +++ b/.vscode/cspell.dictionaries/jargon.wordlist.txt @@ -219,6 +219,9 @@ nofield dtolnay Swatinem +preopened +dotdot + # * clippy uninlined nonminimal diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 063bd7af1ab..632d6ae591d 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -1265,7 +1265,19 @@ fn depth_first_list( false, ), PathData::new( - path_data.path().join("..").into(), + // On WASI the sandbox may block access to ".." at the + // preopened root. Fall back to "." so the entry still + // appears with valid metadata instead of an error. + { + let dotdot = path_data.path().join(".."); + #[cfg(target_os = "wasi")] + let dotdot = if dotdot.metadata().is_err() { + path_data.path().into() + } else { + dotdot + }; + dotdot.into() + }, None, Some(OsStr::new("..").into()), config, diff --git a/tests/by-util/test_ls.rs b/tests/by-util/test_ls.rs index fbbba1a0a22..fe056008d58 100644 --- a/tests/by-util/test_ls.rs +++ b/tests/by-util/test_ls.rs @@ -7133,3 +7133,18 @@ fn test_ls_non_utf8_hidden() { scene.ucmd().succeeds().stdout_does_not_contain(".hidden"); } + +#[test] +#[cfg(target_os = "wasi")] +fn test_ls_a_dotdot_no_error_on_wasi() { + // On WASI the sandbox may block access to ".." at the preopened root. + // ls -a should still succeed and show ".." without an error message. + let scene = TestScenario::new(util_name!()); + scene + .ucmd() + .arg("-a") + .arg("-1") + .succeeds() + .stdout_contains("..") + .no_stderr(); +} From db47beea44aee85f3b6066ed8038c808f21a0716 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sat, 4 Apr 2026 10:40:32 +0200 Subject: [PATCH 201/264] build: embed all locale files when targeting WASI When cross-compiling for wasm32-wasip1, embed every available .ftl file instead of just en-US + the host LANG. This allows the playground to switch languages at runtime. --- src/uucore/build.rs | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/uucore/build.rs b/src/uucore/build.rs index 15068f28aab..f3b4df331a7 100644 --- a/src/uucore/build.rs +++ b/src/uucore/build.rs @@ -347,6 +347,47 @@ fn embed_locale_file( /// /// Returns an error if `for_each_locale` fails, which typically happens if /// reading a locale file or writing to the `embedded_file` fails. +/// Check if we are cross-compiling for WASI (build.rs runs on the host, +/// so `#[cfg(target_os = "wasi")]` does not work here). +fn is_wasi_target() -> bool { + env::var("CARGO_CFG_TARGET_OS") + .map(|os| os == "wasi") + .unwrap_or(false) +} + +/// For WASI/WASM builds, embed ALL available .ftl files in a locale +/// directory so the playground can switch languages at runtime. +fn embed_all_locales_for_component( + embedded_file: &mut File, + component_name: &str, + path_builder: &F, +) -> Result<(), Box> +where + F: Fn(&str) -> PathBuf, +{ + let en_path = path_builder("en-US"); + if let Some(locale_dir) = en_path.parent() { + if locale_dir.exists() { + for entry in std::fs::read_dir(locale_dir)? { + let entry = entry?; + let path = entry.path(); + if path.extension().is_some_and(|e| e == "ftl") { + if let Some(locale) = path.file_stem().and_then(|s| s.to_str()) { + embed_locale_file( + embedded_file, + &path, + &format!("{component_name}/{locale}.ftl"), + locale, + component_name, + )?; + } + } + } + } + } + Ok(()) +} + fn embed_component_locales( embedded_file: &mut File, locales: &(String, Option), @@ -356,6 +397,10 @@ fn embed_component_locales( where F: Fn(&str) -> PathBuf, { + if is_wasi_target() { + return embed_all_locales_for_component(embedded_file, component_name, &path_builder); + } + for_each_locale(locales, |locale| { let locale_path = path_builder(locale); embed_locale_file( From b131f90082868fcbb070f2f5a8ca6d15eae9fb81 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sat, 4 Apr 2026 11:09:03 +0200 Subject: [PATCH 202/264] uucore: add create_wasi_bundle_from_embedded for any locale Add a WASI-specific function that builds a FluentBundle from embedded .ftl files for any locale. Uses Box::leak instead of the global OnceLock cache so multiple locale bundles can coexist. --- src/uucore/src/lib/mods/locale.rs | 35 +++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/uucore/src/lib/mods/locale.rs b/src/uucore/src/lib/mods/locale.rs index c6117c80bff..c846b76e1b9 100644 --- a/src/uucore/src/lib/mods/locale.rs +++ b/src/uucore/src/lib/mods/locale.rs @@ -308,6 +308,41 @@ fn create_english_bundle_from_embedded( } } +/// Create a bundle from embedded locale files for any locale on WASI. +/// Bypasses the global OnceLock cache (uses Box::leak) so it can be +/// called for multiple locales in the same process. +#[cfg(target_os = "wasi")] +fn create_wasi_bundle_from_embedded( + locale: &LanguageIdentifier, + util_name: &str, +) -> Result, LocalizationError> { + let locale_str = locale.to_string(); + let mut bundle: FluentBundle<&'static FluentResource> = FluentBundle::new(vec![locale.clone()]); + bundle.set_use_isolating(false); + + let mut try_add = |key: &str| { + if let Some(content) = get_embedded_locale(key) { + if let Ok(resource) = FluentResource::try_new(content.to_string()) { + bundle.add_resource_overriding(Box::leak(Box::new(resource))); + } + } + }; + + try_add(&format!("uucore/{locale_str}.ftl")); + if util_name.ends_with("sum") { + try_add(&format!("checksum_common/{locale_str}.ftl")); + } + try_add(&format!("{util_name}/{locale_str}.ftl")); + + if bundle.has_message("common-error") || bundle.has_message(&format!("{util_name}-about")) { + Ok(bundle) + } else { + Err(LocalizationError::LocalesDirNotFound(format!( + "No embedded locale found for {util_name}/{locale_str}" + ))) + } +} + fn get_message_internal(id: &str, args: Option) -> String { LOCALIZER.with(|lock| { lock.get() From df08775c0166332177547078fd6631c98c4c61c9 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sat, 4 Apr 2026 11:10:04 +0200 Subject: [PATCH 203/264] uucore: use embedded locale bundles on WASI for l10n support In setup_localization(), use create_wasi_bundle_from_embedded() on WASI to set up the requested locale with English fallback, enabling runtime language switching in the playground. --- src/uucore/src/lib/mods/locale.rs | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/uucore/src/lib/mods/locale.rs b/src/uucore/src/lib/mods/locale.rs index c846b76e1b9..f25866bc144 100644 --- a/src/uucore/src/lib/mods/locale.rs +++ b/src/uucore/src/lib/mods/locale.rs @@ -481,12 +481,27 @@ pub fn setup_localization(p: &str) -> Result<(), LocalizationError> { // Load both utility-specific and common strings init_localization(&locale, &locales_dir, p)?; } else { - // No locales directory found, use embedded English with common strings directly + // No locales directory found, use embedded locales let default_locale = LanguageIdentifier::from_str(DEFAULT_LOCALE) .expect("Default locale should always be valid"); - let english_bundle: FluentBundle<&'static FluentResource> = - create_english_bundle_from_embedded(&default_locale, p)?; - let localizer = Localizer::new(english_bundle); + + #[cfg(target_os = "wasi")] + let localizer = { + let english_bundle = create_wasi_bundle_from_embedded(&default_locale, p)?; + if locale == default_locale { + Localizer::new(english_bundle) + } else if let Ok(localized) = create_wasi_bundle_from_embedded(&locale, p) { + Localizer::new(localized).with_fallback(english_bundle) + } else { + Localizer::new(english_bundle) + } + }; + + #[cfg(not(target_os = "wasi"))] + let localizer = { + let english_bundle = create_english_bundle_from_embedded(&default_locale, p)?; + Localizer::new(english_bundle) + }; LOCALIZER.with(|lock| { lock.set(localizer) From b2691021a92bac72218107415e8ac029400a7d9f Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Sun, 5 Apr 2026 01:34:38 +0900 Subject: [PATCH 204/264] wc: drop #[allow(clippy::question_mark)] --- src/uu/wc/src/count_fast.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/uu/wc/src/count_fast.rs b/src/uu/wc/src/count_fast.rs index ea334a8e72e..637a777e4fe 100644 --- a/src/uu/wc/src/count_fast.rs +++ b/src/uu/wc/src/count_fast.rs @@ -52,7 +52,6 @@ fn count_bytes_using_splice(fd: &impl AsFd) -> Result { Ok(res) => { byte_count += res; // Silent the warning as we want to the error message - #[allow(clippy::question_mark)] if splice_exact(&pipe_rd, &null_file, res).is_err() { return Err(byte_count); } From 02415904f14592489b88526687f15ce94c6534ce Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sat, 4 Apr 2026 22:38:00 +0200 Subject: [PATCH 205/264] add 0.7.0 release notes in tree --- docs/src/release-notes/0.7.0.md | 518 ++++++++++++++++++++++++++++++++ 1 file changed, 518 insertions(+) create mode 100644 docs/src/release-notes/0.7.0.md diff --git a/docs/src/release-notes/0.7.0.md b/docs/src/release-notes/0.7.0.md new file mode 100644 index 00000000000..2a8a858340e --- /dev/null +++ b/docs/src/release-notes/0.7.0.md @@ -0,0 +1,518 @@ +### **Rust Coreutils 0.7.0 Release:** + +We are excited to announce the release of **Rust Coreutils 0.7.0** — a performance-focused release with **major optimizations across dozens of utilities**, continued safety improvements replacing unsafe code with safe abstractions, and a comprehensive campaign to eliminate panics on write errors. We also contributed many patches upstream to GNU coreutils, and welcomed their feedbacks and supports, strengthening both projects! + +--- + +### GNU Test Suite Compatibility: + +| Result | 0.6.0 | 0.7.0 | Change 0.6.0 to 0.7.0 | % Total 0.6.0 | % Total 0.7.0 | % Change 0.6.0 to 0.7.0 | +|---------------|-------|-------|------------------------|---------------|---------------|--------------------------| +| Pass | 622 | 629 | +7 | 96.28% | 94.59% | -1.69% | +| Skip | 7 | 13 | +6 | 1.08% | 1.95% | +0.87% | +| Fail | 16 | 23 | +7 | 2.48% | 3.46% | +0.98% | +| Error | 1 | 0 | -1 | 0.15% | 0% | -0.15% | +| Total | 646 | 665 | +19 (new tests) | | | | + +> **Note:** The GNU test reference was updated from 9.9 to 9.10, adding **19 new tests**. While the pass percentage decreased due to these newly added tests, the absolute number of passing tests increased by 7 and errors were eliminated entirely. Work is ongoing to address the new test failures. + +--- + +![GNU testsuite evolution](https://github.com/uutils/coreutils-tracking/blob/main/gnu-results.svg?raw=true) + +--- + +### Highlights: + +- **GNU Compatibility & Upstream Contributions** + - **629 passing tests** (+7 from 0.6.0), with **19 new tests** added from the GNU 9.10 update + - Updated GNU test reference from 9.9 to 9.10 + - Contributed numerous patches upstream to GNU coreutils, benefiting both projects + - New GNU compatibility fixes across `date`, `fmt`, `kill`, `ptx`, `numfmt`, `cksum`, and more + - Took over maintenance of [`num-prime`](https://github.com/uutils/num-prime), the primality testing library used by `factor` + +- **Performance Overhaul** + - Faster hash maps: `rustc-hash` in `ls`, `du`, `tsort`, `shuf`, `mv`; `foldhash` in `sort` + - `unexpand`/`expand`: ASCII fast-path, buffered reads — 14%+ gain in `unexpand` + - `shuf`, `split`, `sort`, `du`: Reduced malloc allocations (+3–6% in `du`, +4% in `shuf`) + - `nl`: Optimized with `itoa` and direct writing + - `true`/`false`: Removed `clap` dependency, smaller binary, faster startup + - `uucore`: Disabled signal setup in simple utilities for binary size and startup speed + +- **Robustness: Eliminated `/dev/full` Panics** + - Fixed panics when stderr is `/dev/full` across **20+ utilities** (`echo`, `date`, `sort`, `expr`, `hostname`, `id`, `comm`, `pr`, `dircolors`, and more) + - Generic fix ensuring unrecognized options with `2>/dev/full` do not abort + +- **Safety & Code Quality** + - Replaced unsafe `libc` calls with safe `nix` crate wrappers in `uucore` (`umask`, `mkdirat`, and more) and `mknod` + - Eliminated TOCTOU races in `ln`, `tac`, and `install -D` + - `rm`: `--preserve-root` now works correctly on symlinks + - MSRV updated to **1.88** + +- **Notable Bug Fixes** + - `date`: Extensive fixes — `-u`/`-s`/`-d` flags, timezone abbreviation lookup and DST, RFC-822 format, `%+`/`%_` modifiers, `--debug`, locale `date_fmt` + - `cp`: Readonly directories, `-a`/`-z` flags, special files, non-UTF-8 directory names + - `mv`: Preserve symlinks during cross-device moves, handle FIFOs in directories + - `ls`: Hyperlink OSC 8 format, dired reports, fd leak on deep recursion, invalid UTF-8 hidden files + - `sort`: Collator panic in worker threads, scientific notation parsing + - `paste`: Multi-byte delimiters, GNU escape sequences, bounded buffering + - `printf`: `%q` shell quoting with control chars and quotes + - `ptx`: `-t`/`--typeset-mode`, multibyte Unicode panic, GNU default behavior + - `numfmt`: `--debug` flag, empty delimiter, null byte handling, error message formatting + - `cksum`: SHAKE algorithms, `--binary`/`--text`/`--tag` errors + - `cut`, `tac`, `tail`, `tr`, `uniq`, `od`, `chroot`, `stat`, `mktemp`, `pr`, `readlink`, `ln`, `kill`, `nproc`, `rm`, `env`, `sync`, `fmt`, `factor`, `wc`: Various GNU compatibility and correctness fixes + +- **Platform Support** + - NetBSD and PowerPC build fixes + - Windows: `tac` stdin piping, `test -r/-w/-x`, publish static `*.exe` binaries + - WebAssembly: Publish `*.wasm` artifacts + - `stdbuf`: Support `libstdbuf` in same directory as binary + - NixOS test compatibility fix; added security audit workflow + +- **Contributions**: This release was made possible by **23 new contributors** joining our community + +--- + +### Call to Action: + +**Help us translate** - Contribute translations at [Weblate](https://hosted.weblate.org/projects/rust-coreutils/) +**Sponsor us on GitHub** to accelerate development: [github.com/sponsors/uutils](https://github.com/sponsors/uutils) + +## What's Changed + +## cat +* cat: strip errno by @oech3 in https://github.com/uutils/coreutils/pull/10885 + +## cksum +* *sum: Fix locales fetching from `checksum_common` after installation by @RenjiSann in https://github.com/uutils/coreutils/pull/10575 +* *sum: Fix read_byte_lines discarding read errors by @RenjiSann in https://github.com/uutils/coreutils/pull/10671 +* test/cksum: implement `test_signed_checksums` by @0xMillyByte in https://github.com/uutils/coreutils/pull/10714 +* cksum: Accept SHAKE algorithms by @RenjiSann in https://github.com/uutils/coreutils/pull/10772 +* cksum family: Backport new errors for --binary, --text and --tag by @oech3 in https://github.com/uutils/coreutils/pull/10618 +* cksum family: Fix clippy::unnecessary_wraps by @oech3 in https://github.com/uutils/coreutils/pull/11110 + +## chroot +* chroot: fix gid being set by uid with --userspec #10307 by @cerdelen in https://github.com/uutils/coreutils/pull/10465 +* chroot: use var_os by @xtqqczze in https://github.com/uutils/coreutils/pull/11070 + +## comm +* comm /etc/pacman.conf /dev/null 2>/dev/full does not abort by @oech3 in https://github.com/uutils/coreutils/pull/10746 +* date, comm, tty: Fixing handling output to dev/null by @ChrisDryden in https://github.com/uutils/coreutils/pull/10888 + +## coreutils +* tests/misc/coreutils.sh: Fail with invalid binary name by @oech3 in https://github.com/uutils/coreutils/pull/10258 +* coreutils: output expected error for unrecognized options by @ChrisDryden in https://github.com/uutils/coreutils/pull/9869 +* coreutils: Let the name *utils valid by @oech3 in https://github.com/uutils/coreutils/pull/10729 +* coreutils: Fix 2>/dev/full aborts & drop a sed for GnuTests by @oech3 in https://github.com/uutils/coreutils/pull/10740 +* all: --typo 2>/dev/full does not abort by @oech3 in https://github.com/uutils/coreutils/pull/10764 +* Add regression test for coreutils --list by @oech3 in https://github.com/uutils/coreutils/pull/10858 + +## cp +* cp: fix recursive copy of readonly directories by @nikolalukovic in https://github.com/uutils/coreutils/pull/10529 +* cp: improve code clarity and remove redundant filesystem checks by @sylvestre in https://github.com/uutils/coreutils/pull/10790 +* cp: fixing cp -a functionality to match gnu implementation for -z flag handling and for folders by @ChrisDryden in https://github.com/uutils/coreutils/pull/10207 +* cp: Fix panic when recursively copying a directory with a non-UTF8 name by @aweinstock314 in https://github.com/uutils/coreutils/pull/11148 +* cp: handle special files by @victor-prokhorov in https://github.com/uutils/coreutils/pull/11163 + +## csplit +* refactor(csplit): use &str slices for patterns by @xtqqczze in https://github.com/uutils/coreutils/pull/11013 +* csplit: add benchmarks for line number and regex pattern splitting by @sylvestre in https://github.com/uutils/coreutils/pull/10927 + +## cut +* cut: fix -s flag for newline delimiter and optimize memory allocation by @akervald in https://github.com/uutils/coreutils/pull/11143 +* cut: two simple refactorings by @cakebaker in https://github.com/uutils/coreutils/pull/11194 + +## date +* date: fix -u flag to match GNU behavior for input parsing by @ChrisDryden in https://github.com/uutils/coreutils/pull/10715 +* date: Fix format optional argument to capture all following parameters by @cerdelen in https://github.com/uutils/coreutils/pull/10914 +* date: fix -s UTC conversion losing timezone offset by @yachi in https://github.com/uutils/coreutils/pull/10828 +* date: fix RFC-822 format to always use English names by @sylvestre in https://github.com/uutils/coreutils/pull/10932 +* date: bump parse_datetime & add test for leap-1 GNU test by @sylvestre in https://github.com/uutils/coreutils/pull/10933 +* date: add tests to match GNU's by @sylvestre in https://github.com/uutils/coreutils/pull/10939 +* date: fix double periods in Hungarian month abbreviations by @naoNao89 in https://github.com/uutils/coreutils/pull/10945 +* date: fix subfmt-up1, fill-1, pct-pct, and invalid-high-bit-set tests / implement --debug by @sylvestre in https://github.com/uutils/coreutils/pull/10940 +* date: use locale date_fmt instead of D_T_FMT for default format by @ChrisDryden in https://github.com/uutils/coreutils/pull/10935 +* date: Fix Error message missing '+' for format string after valid d flag by @cerdelen in https://github.com/uutils/coreutils/pull/10982 +* date: fix -d with relative dates and timezone abbreviations by @yachi in https://github.com/uutils/coreutils/pull/10956 +* date: fix timezone abbreviations using wrong offset outside their DST season by @aguimaraes in https://github.com/uutils/coreutils/pull/11045 +* date: fix %+ and %_ modifier edge cases by @naoNao89 in https://github.com/uutils/coreutils/pull/10999 +* date: extend tz abbreviation lookup by @cerdelen in https://github.com/uutils/coreutils/pull/11229 +* date: Remove eprintln! to avoid 2>/dev/full abort by @oech3 in https://github.com/uutils/coreutils/pull/11228 +* date, comm, tty: Fixing handling output to dev/null by @ChrisDryden in https://github.com/uutils/coreutils/pull/10888 + +## dd +* dd: simplify signal handling by removing Alarm timer thread by @ChrisDryden in https://github.com/uutils/coreutils/pull/10768 + +## df +* df: fallback when proc masked by @ChrisDryden in https://github.com/uutils/coreutils/pull/10417 + +## dircolors +* dircolors >/dev/full panics by @oech3 in https://github.com/uutils/coreutils/pull/10948 + +## du +* du: deduplicate Stat::new call by @svlv in https://github.com/uutils/coreutils/pull/10584 +* du: Use rustc-hash for du -a / performance by @oech3 in https://github.com/uutils/coreutils/pull/10663 +* du: Flags 'm', 'k', 'm' should be POSIX style overriden by @cerdelen in https://github.com/uutils/coreutils/pull/10664 +* Du size_format flag override by @cerdelen in https://github.com/uutils/coreutils/pull/10743 +* du: malloc perf +3~6% by @oech3 in https://github.com/uutils/coreutils/pull/11034 + +## echo +* echo --version >/dev/full panics by @oech3 in https://github.com/uutils/coreutils/pull/10853 + +## env +* env: fix regression of `--ignore-signal=PIPE` by @Ecordonnier in https://github.com/uutils/coreutils/pull/9618 + +## expand +* expand: remove read_until by @cerdelen in https://github.com/uutils/coreutils/pull/10657 +* expand: remove empty after help by @cakebaker in https://github.com/uutils/coreutils/pull/10977 +* expand: Fix performance drop with cgu=1 by ascii fast-path by @oech3 in https://github.com/uutils/coreutils/pull/11104 + +## expr +* expr --version >/dev/full panics & --help > /dev/full should fail by @oech3 in https://github.com/uutils/coreutils/pull/10854 + +## factor +* factor: add a test for a num-prime issue by @sylvestre in https://github.com/uutils/coreutils/pull/11096 +* factor: trim also null-chars by @yotam-medini in https://github.com/uutils/coreutils/pull/11182 + +## false +* false: dedup set_exit_code(1) by @oech3 in https://github.com/uutils/coreutils/pull/10823 +* true, false: Fix broken pipe by @oech3 in https://github.com/uutils/coreutils/pull/11204 +* true, false: drop Vec for binary size and perf by @oech3 in https://github.com/uutils/coreutils/pull/11223 + +## fmt +* fmt: restore GNU compatibility for tests/fmt/width by @karanabe in https://github.com/uutils/coreutils/pull/11073 + +## fold +* fold: refactor compute_col_count and add character mode tests by @ChrisDryden in https://github.com/uutils/coreutils/pull/10533 +* fold: replace truncate(0) with clear() by @xtqqczze in https://github.com/uutils/coreutils/pull/11059 + +## hostname +* hostname: fix panic on hostname > /dev/full by @WhateverAWS in https://github.com/uutils/coreutils/pull/10912 +* feat(hostname): add benchmark with large /etc/hosts by @naoNao89 in https://github.com/uutils/coreutils/pull/10979 + +## id +* Id: don't panic on write error by @FidelSch in https://github.com/uutils/coreutils/pull/10769 + +## install +* Fixed permissions in install for unix by @max-amb in https://github.com/uutils/coreutils/pull/10564 +* fix(install): prevent symlink race condition in install -D (fixes #10013) by @abendrothj in https://github.com/uutils/coreutils/pull/10140 + +## kill +* kill: fix GNU compatibility tests for RTMIN and RTMAX by @karanabe in https://github.com/uutils/coreutils/pull/11224 + +## ln +* fix: eliminate TOCTOU races in ln and tac by deferring is_dir() checks by @abendrothj in https://github.com/uutils/coreutils/pull/10991 +* ln: Interactive and Force override each other instead of defaulting to Force if both are specified by @aweinstock314 in https://github.com/uutils/coreutils/pull/11129 + +## ls +* ls: Replace Fnv with rustc-hash by @oech3 in https://github.com/uutils/coreutils/pull/10686 +* ls: fix hyperlink functionality to use correct OSC 8 format and handle symlink targets by @sylvestre in https://github.com/uutils/coreutils/pull/10824 +* ls: fix ls dired reports by @mattsu2020 in https://github.com/uutils/coreutils/pull/10527 +* ls: release directory fds before recursing to avoid EMFILE by @ChrisDryden in https://github.com/uutils/coreutils/pull/10894 +* ls: Update French translations by @sylvestre in https://github.com/uutils/coreutils/pull/11003 +* ls: Use rustc-hash at colors by @oech3 in https://github.com/uutils/coreutils/pull/10700 +* ls: Treat paths starting with a dot as hidden even if they contain invalid UTF-8. by @aweinstock314 in https://github.com/uutils/coreutils/pull/11135 + +## mktemp +* mktemp: handle invalid UTF-8 in suffix gracefully by @sylvestre in https://github.com/uutils/coreutils/pull/10818 +* mktemp: use env::os_var by @xtqqczze in https://github.com/uutils/coreutils/pull/11113 +* mktemp: Don't panic when getrandom failed by @oech3 in https://github.com/uutils/coreutils/pull/11154 + +## mknod +* mKnod: Refactor to remove unsafe code by @mattsu2020 in https://github.com/uutils/coreutils/pull/10138 + +## mv +* mv: handle FIFOs inside directories during cross-partition move by @ChrisDryden in https://github.com/uutils/coreutils/pull/10857 +* fix(mv): correct selinux cfg gating for non-Linux platforms by @naoNao89 in https://github.com/uutils/coreutils/pull/10989 +* mv: Use rustc-hash by @oech3 in https://github.com/uutils/coreutils/pull/11010 +* mv: preserve symlinks during cross-device moves instead of expanding them by @sylvestre in https://github.com/uutils/coreutils/pull/10546 + +## nl +* perf(nl): optimize line numbering by using itoa and direct writing by @CrazyRoka in https://github.com/uutils/coreutils/pull/10757 + +## nproc +* nproc: process space in OMP_NUM_THREADS by @cuiweixie in https://github.com/uutils/coreutils/pull/10973 +* nproc: Cleanup a const by @oech3 in https://github.com/uutils/coreutils/pull/11191 +* nproc: Minor cleanup by @oech3 in https://github.com/uutils/coreutils/pull/11192 + +## numfmt +* numfmt: add --debug flag to print warnings about invalid input by @ChrisDryden in https://github.com/uutils/coreutils/pull/10110 +* numfmt: numfmt --debug 2>/dev/full does not abort by @tuananh in https://github.com/uutils/coreutils/pull/10668 +* numfmt: fix empty delimiter and whitespace handling by @ChrisDryden in https://github.com/uutils/coreutils/pull/10350 +* numfmt: optimize output handling by using stdout directly by @xtqqczze in https://github.com/uutils/coreutils/pull/11051 +* fix(numfmt): Read lines only up to null byte (as GNU does) by @FidelSch in https://github.com/uutils/coreutils/pull/11146 +* fix(numfmt): format output on error messages by @FidelSch in https://github.com/uutils/coreutils/pull/11179 +* numfmt: stop a clone by @oech3 in https://github.com/uutils/coreutils/pull/11221 + +## od +* od: fix od -v /dev/zero panic by @mattsu2020 in https://github.com/uutils/coreutils/pull/10576 + +## paste +* paste: support multi-byte delimiters and GNU escape sequences by @ChrisDryden in https://github.com/uutils/coreutils/pull/10840 +* paste: avoid unbounded buffering for single input by @mattsu2020 in https://github.com/uutils/coreutils/pull/11060 + +## pr +* pr: fix column behavior for short files by @jfinkels in https://github.com/uutils/coreutils/pull/10379 +* pr missing 2>/dev/full does not abort by @oech3 in https://github.com/uutils/coreutils/pull/10731 +* pr: implement the -e flag by @cerdelen in https://github.com/uutils/coreutils/pull/10167 + +## printf +* printf: fix %q shell quoting with control chars and quotes by @sylvestre in https://github.com/uutils/coreutils/pull/10816 + +## ptx +* ptx: implement -t/--typeset-mode to change default width to 100 by @ChrisDryden in https://github.com/uutils/coreutils/pull/10856 +* ptx: fix panic when truncation string/keyword contain multibyte Unicode by @Xylphy in https://github.com/uutils/coreutils/pull/10836 +* ptx: match GNU default behavior by skipping non-alphabetic index tokens by @Xylphy in https://github.com/uutils/coreutils/pull/10919 + +## readlink +* readlink: Set silent mode as default by @denendaden in https://github.com/uutils/coreutils/pull/10711 + +## rm +* rm --preserve-root should work on symlink too by @sylvestre in https://github.com/uutils/coreutils/pull/9706 +* rm: report permission denied for unreadable subdirectories by @o1x3 in https://github.com/uutils/coreutils/pull/10974 + +## shuf +* shuf: Use rustc-hash for performance by @oech3 in https://github.com/uutils/coreutils/pull/10648 +* shuf: Drop inline after switched to fxhash by @oech3 in https://github.com/uutils/coreutils/pull/10781 +* shuf: Reduce malloc by @oech3 in https://github.com/uutils/coreutils/pull/10998 +* shuf: delete useless cast by @oech3 in https://github.com/uutils/coreutils/pull/11106 +* shuf: try Vec first and fallback to HashMap if it cause OOM by @oech3 in https://github.com/uutils/coreutils/pull/11169 +* shuf: Reduce malloc, perf +4% by @oech3 in https://github.com/uutils/coreutils/pull/11219 + +## sort +* sort: remove redundant if by @oech3 in https://github.com/uutils/coreutils/pull/10716 +* Sort: Use ahash by @oech3 in https://github.com/uutils/coreutils/pull/10783 +* sort: making ClosedCompressedTmpFile::reopen() not panic. by @devnexen in https://github.com/uutils/coreutils/pull/10807 +* sort: fix panic when collator not available in worker threads by @sylvestre in https://github.com/uutils/coreutils/pull/10915 +* fix: scientific notation is incorrectly parsed in general numeric sort by @Kaua-Klassmann in https://github.com/uutils/coreutils/pull/10437 +* sort: use LazyLock by @xtqqczze in https://github.com/uutils/coreutils/pull/10969 +* Sort debug message by @hlsxx in https://github.com/uutils/coreutils/pull/10960 +* sort: Replace malloc and 0 fill with huge reserve & min 0 fill by @oech3 in https://github.com/uutils/coreutils/pull/10975 +* sort: remove reserve which is difficult to understand by @oech3 in https://github.com/uutils/coreutils/pull/11040 +* sort: update DEFAULT_BUF_SIZE to 8 KiB by @xtqqczze in https://github.com/uutils/coreutils/pull/11041 +* sort: failed to set message sort by @hlsxx in https://github.com/uutils/coreutils/pull/11137 +* sort --compress-program missing 2>/dev/full does not abort by @oech3 in https://github.com/uutils/coreutils/pull/10951 +* deps: replace ahash with foldhash by @xtqqczze in https://github.com/uutils/coreutils/pull/11187 + +## split +* perf(split): reuse buffer in chunked splitting loop by @CrazyRoka in https://github.com/uutils/coreutils/pull/10695 +* perf(split): optimize FixedWidthNumber Display implementation by @CrazyRoka in https://github.com/uutils/coreutils/pull/10723 +* split: Reduce malloc by @oech3 in https://github.com/uutils/coreutils/pull/10976 + +## stat +* stat: fix mount table read when /proc is unavailable by @sylvestre in https://github.com/uutils/coreutils/pull/10300 +* print_numeric: print mode in octal when the mode is too large by @Connor-GH in https://github.com/uutils/coreutils/pull/10208 + +## stdbuf +* stdbuf: support libstdbuf in same directory as stdbuf by @Ecordonnier in https://github.com/uutils/coreutils/pull/10352 +* stdbuf: fix warning from `uninlined_format_args` by @cakebaker in https://github.com/uutils/coreutils/pull/10784 + +## stty +* stty: cleanup cfg by @oech3 in https://github.com/uutils/coreutils/pull/10879 +* stty: fix compilation on PowerPC architectures by @sylvestre in https://github.com/uutils/coreutils/pull/11050 + +## sync +* sync: handle fcntl errors with localized warnings by @sylvestre in https://github.com/uutils/coreutils/pull/10330 +* sync: open file with nonblock by @reubenwong97 in https://github.com/uutils/coreutils/pull/10765 +* sync: return after checking all inputs by @reubenwong97 in https://github.com/uutils/coreutils/pull/10742 + +## tac +* tac: fix stdin piping on Windows by skipping empty mmap results by @ChrisDryden in https://github.com/uutils/coreutils/pull/10795 +* tac: support non-UTF-8 separator by @victor-prokhorov in https://github.com/uutils/coreutils/pull/10934 + +## tail +* tail: report PermissionDenied instead of No such file when metadata fails by @RedNhight in https://github.com/uutils/coreutils/pull/11018 +* tests/tail: reduce delays in multiple tests to speed up execution by @domysu in https://github.com/uutils/coreutils/pull/11206 + +## test +* test: make the man-page readable + sync by @matttbe in https://github.com/uutils/coreutils/pull/10737 +* test: implement -r/-w/-x on Windows by @anihal in https://github.com/uutils/coreutils/pull/11120 + +## tr +* tr: fix possible usage in invalid utf8 set sequence. by @devnexen in https://github.com/uutils/coreutils/pull/10791 +* tr: simplify truncate logic by @xtqqczze in https://github.com/uutils/coreutils/pull/11072 + +## true +* true/false: remove large clap call by @my4ng in https://github.com/uutils/coreutils/pull/10673 +* true, false: Improve perf & fix clippy::unnecessary_wraps by @oech3 in https://github.com/uutils/coreutils/pull/11200 +* feat(true/false): add benchmarks for startup performance by @naoNao89 in https://github.com/uutils/coreutils/pull/10996 + +## tsort +* tsort: Use rustc-hash by @oech3 in https://github.com/uutils/coreutils/pull/10680 + +## unexpand +* Unexpand use buffered reads + tests by @cerdelen in https://github.com/uutils/coreutils/pull/10831 +* unexpand: codegen-units=1 by @oech3 in https://github.com/uutils/coreutils/pull/10817 +* unexpand: Refactor functions to use less parameters by @cerdelen in https://github.com/uutils/coreutils/pull/10900 +* unexpand: ascii fast-path by @oech3 in https://github.com/uutils/coreutils/pull/11114 +* unexpand: reuse a smaller buf, perf 14%+ by @oech3 in https://github.com/uutils/coreutils/pull/11178 + +## uniq +* uniq: fix -w to count bytes in C locale by @aguimaraes in https://github.com/uutils/coreutils/pull/11061 + +## uptime +* uptime: fix "unused import" warnings in test file by @cakebaker in https://github.com/uutils/coreutils/pull/10651 +* uptime: Remove wincode and wincode-derive dependencies by @ChrisDryden in https://github.com/uutils/coreutils/pull/10802 +* fix: uptime > /dev/full panic fix by @hlsxx in https://github.com/uutils/coreutils/pull/10827 + +## vdir +* vdir: make vdir an alias of ls to resolve missing ftl by @NatsuCamellia in https://github.com/uutils/coreutils/pull/10434 + +## wc +* wc: stop processing --files0-from input after stdout write failure by @mattsu2020 in https://github.com/uutils/coreutils/pull/11023 + +## whoami +* whoami: fix usage line for unexpected arguments by @YuF-9468 in https://github.com/uutils/coreutils/pull/11159 + +## uucore +* uucore: refactor digest_reader to use ReadingMode enum by @0xMillyByte in https://github.com/uutils/coreutils/pull/10720 +* uucore: Use `nix::sys::stat::umask` in `uucore::mode::get_umask` by @mattsu2020 in https://github.com/uutils/coreutils/pull/11102 +* uucore: fix correct mkdirat implementation to use nix crate's mkdirat function by @mattsu2020 in https://github.com/uutils/coreutils/pull/11126 +* uucore: replace unsafe libc calls with safe nix crate wrappers by @mattsu2020 in https://github.com/uutils/coreutils/pull/11156 +* uucore: use transmute instead of raw pointers by @xtqqczze in https://github.com/uutils/coreutils/pull/11077 +* uucore: Disallow slashes in determine_backup_suffix by @aweinstock314 in https://github.com/uutils/coreutils/pull/11149 +* uucore: disable signals at simple utils for binary size and fast startup by @oech3 in https://github.com/uutils/coreutils/pull/11186 +* dedup high-cost localization setup by @oech3 in https://github.com/uutils/coreutils/pull/11147 + +## CI & Build +* Catch regressions at early stage by @oech3 in https://github.com/uutils/coreutils/pull/10627 +* ci: enable memory profiling again by @not-matthias in https://github.com/uutils/coreutils/pull/10659 +* Revert "CICD.yml: upload binaries without version string" by @sylvestre in https://github.com/uutils/coreutils/pull/10676 +* Fix tag/latest-commit & comment naming policy by @oech3 in https://github.com/uutils/coreutils/pull/10684 +* fuzzing.yml: Use prebuilt for faster setup by @oech3 in https://github.com/uutils/coreutils/pull/10498 +* chore: run pre-commit on all files by @aaron-ang in https://github.com/uutils/coreutils/pull/10119 +* MSRV 1.88 by @oech3 in https://github.com/uutils/coreutils/pull/10771 +* Drop release-fast profile for simplicity by @oech3 in https://github.com/uutils/coreutils/pull/10476 +* actions: add security audit workflow by @xtqqczze in https://github.com/uutils/coreutils/pull/10767 +* Publish *.wasm by @oech3 in https://github.com/uutils/coreutils/pull/10656 +* Add rust-version field to all Cargo.toml files by @xtqqczze in https://github.com/uutils/coreutils/pull/10778 +* GnuTests: 9.10, remove a bunch of backported tests by @oech3 in https://github.com/uutils/coreutils/pull/10721 +* build: sort coreutils entries at build time by @Xylphy in https://github.com/uutils/coreutils/pull/10820 +* GNUmakefile: Allow cross-build for Windows by @oech3 in https://github.com/uutils/coreutils/pull/10897 +* CICD: Publish static individual *.exe by @oech3 in https://github.com/uutils/coreutils/pull/10903 +* benchmarks.yml: Use prebuilt tools for reproducible benches by @oech3 in https://github.com/uutils/coreutils/pull/10497 +* ci: remove GNU build caching from GnuTests workflow by @ChrisDryden in https://github.com/uutils/coreutils/pull/10961 +* CICD: Introduce check-only for Redox CI by @oech3 in https://github.com/uutils/coreutils/pull/10950 +* rust: use const_locks feature by @xtqqczze in https://github.com/uutils/coreutils/pull/10968 +* uutests: preserve PATH in UCommand to fix NixOS test failures by @0xMillyByte in https://github.com/uutils/coreutils/pull/10959 +* build-gnu: fix factor tests being re-added by automake during make check by @ChrisDryden in https://github.com/uutils/coreutils/pull/10907 +* Prevent make check from rebuilding GNU binaries over uutils ones by @ChrisDryden in https://github.com/uutils/coreutils/pull/11049 +* Disable strip related tests on Android by @oech3 in https://github.com/uutils/coreutils/pull/11047 +* test_env: Make argv0 tests compatible with Ubuntu patch by @oech3 in https://github.com/uutils/coreutils/pull/10834 +* GnuTests: Use our libstdbuf.so by @oech3 in https://github.com/uutils/coreutils/pull/10793 +* Avoid non reproducible cache generation at codcov by @oech3 in https://github.com/uutils/coreutils/pull/10926 +* Use MULTICALL=y at toybox test for faster CI by @oech3 in https://github.com/uutils/coreutils/pull/10370 +* CICD: Drop a duplicated test producing huge caches by @oech3 in https://github.com/uutils/coreutils/pull/10513 +* Don't fail when sccache caused network error & mark 2 tests flakey by @oech3 in https://github.com/uutils/coreutils/pull/11098 +* Don't wrap rustc when sccache action caused network err by @oech3 in https://github.com/uutils/coreutils/pull/11141 +* Cargo.toml: Avoid huge diff generation at version bump by @oech3 in https://github.com/uutils/coreutils/pull/11131 +* Cargo.toml: Define feat_wasm by @oech3 in https://github.com/uutils/coreutils/pull/11074 +* Mark pr/bounded-memory flakey & drop 2 useless setup by @oech3 in https://github.com/uutils/coreutils/pull/10887 +* Mark date-locale-hour flakey & drop a useless cache action by @oech3 in https://github.com/uutils/coreutils/pull/11093 +* Updating intermittent gnu test failures to reflect recent changes by @ChrisDryden in https://github.com/uutils/coreutils/pull/10804 + +## Documentation +* README.md: Don't recommend latest-commit for everyone by @oech3 in https://github.com/uutils/coreutils/pull/10678 +* add 0.5.0 & 0.6.0 release notes by @sylvestre in https://github.com/uutils/coreutils/pull/10709 +* Remove the word: hashsums by @oech3 in https://github.com/uutils/coreutils/pull/10674 +* fix docs hashsumS and test by @Its-Just-Nans in https://github.com/uutils/coreutils/pull/7936 +* head: improve help grammar by @memark in https://github.com/uutils/coreutils/pull/10848 +* Fix for potential typos by @lanceXwq in https://github.com/uutils/coreutils/pull/10822 +* chore: use consistent copyright header in `.rs` files by @xtqqczze in https://github.com/uutils/coreutils/pull/11020 + +## Code Quality & Cleanup +* clippy: fix nightly map_unwrap_or lint by @xtqqczze in https://github.com/uutils/coreutils/pull/10625 +* Re-enable unused_qualifications lint by @Xylphy in https://github.com/uutils/coreutils/pull/10571 +* chore: a few more Clippy fixes by @nyurik in https://github.com/uutils/coreutils/pull/10697 +* chore: minor optimizations by @nyurik in https://github.com/uutils/coreutils/pull/10707 +* chore: fix `clippy::single_match_else` by @nyurik in https://github.com/uutils/coreutils/pull/10699 +* chore: `clippy::unreadable_literal` by @nyurik in https://github.com/uutils/coreutils/pull/10717 +* refactor: removing unnecessary references in fn signatures by @nyurik in https://github.com/uutils/coreutils/pull/10703 +* chore: `clippy::wildcard_imports` by @nyurik in https://github.com/uutils/coreutils/pull/10719 +* chore: `clippy::unnecessary_wraps` by @nyurik in https://github.com/uutils/coreutils/pull/10722 +* chore: a few minor lints by @nyurik in https://github.com/uutils/coreutils/pull/10754 +* chore: `clippy::redundant_closure_for_method_calls` by @nyurik in https://github.com/uutils/coreutils/pull/10704 +* refactor: inline format! args in a few places by @nyurik in https://github.com/uutils/coreutils/pull/10730 +* clippy: fix manual_is_multiple_of lint by @xtqqczze in https://github.com/uutils/coreutils/pull/10775 +* clippy: fix struct_field_names lint by @xtqqczze in https://github.com/uutils/coreutils/pull/10902 +* clippy: fix nightly lints by @xtqqczze in https://github.com/uutils/coreutils/pull/10970 +* clippy: fix lints by @xtqqczze in https://github.com/uutils/coreutils/pull/11000 +* clippy: fix nightly lints by @xtqqczze in https://github.com/uutils/coreutils/pull/11232 +* clippy: enable `struct_field_names` lint by @cakebaker in https://github.com/uutils/coreutils/pull/11214 +* chore: Enable workspace lints in uutest crate by @Xylphy in https://github.com/uutils/coreutils/pull/10727 +* deps: remove unused rust deps by @xtqqczze in https://github.com/uutils/coreutils/pull/10688 +* use zip for cleaner iteration with 1-based index by @xtqqczze in https://github.com/uutils/coreutils/pull/10963 +* refactor: simplify UError construction by @xtqqczze in https://github.com/uutils/coreutils/pull/11171 +* refactor: use slice::get for clarity by @xtqqczze in https://github.com/uutils/coreutils/pull/11108 +* tail: disallow clippy::wrong_self_convention by @oech3 in https://github.com/uutils/coreutils/pull/11145 +* Remove hashsum test files by @oech3 in https://github.com/uutils/coreutils/pull/11086 + +## Dependency Updates +* Bump wincode crates to `0.3.1` by @cakebaker in https://github.com/uutils/coreutils/pull/10650 +* chore(deps): update rust crate regex to v1.12.3 by @renovate[bot] in https://github.com/uutils/coreutils/pull/10683 +* chore(deps): update rust crate clap to v4.5.57 by @renovate[bot] in https://github.com/uutils/coreutils/pull/10685 +* chore(deps): update rust crate zip to v7.3.0 by @renovate[bot] in https://github.com/uutils/coreutils/pull/10713 +* chore(deps): update vmactions/freebsd-vm action to v1.3.9 by @renovate[bot] in https://github.com/uutils/coreutils/pull/9605 +* chore(deps): update rust crate jiff to v0.2.19 by @renovate[bot] in https://github.com/uutils/coreutils/pull/10760 +* chore(deps): update rust crate memchr to v2.8.0 by @renovate[bot] in https://github.com/uutils/coreutils/pull/10774 +* chore(deps): update rust crate libc to v0.2.181 by @renovate[bot] in https://github.com/uutils/coreutils/pull/10844 +* Revert "chore(deps): update rust crate libc to v0.2.181" by @xtqqczze in https://github.com/uutils/coreutils/pull/10875 +* chore(deps): update rust crate clap to v4.5.58 by @renovate[bot] in https://github.com/uutils/coreutils/pull/10876 +* chore(deps): update rust crate clap_complete to v4.5.66 by @renovate[bot] in https://github.com/uutils/coreutils/pull/10877 +* Bump `jiff` & fix clippy warning by @cakebaker in https://github.com/uutils/coreutils/pull/10881 +* chore(deps): update rust crate tempfile to v3.25.0 by @renovate[bot] in https://github.com/uutils/coreutils/pull/10843 +* chore(deps): update rust crate libfuzzer-sys to v0.4.12 by @renovate[bot] in https://github.com/uutils/coreutils/pull/10864 +* chore(deps): update rust crate z85 to v3.0.7 by @renovate[bot] in https://github.com/uutils/coreutils/pull/10913 +* chore(deps): update rust crate zip to v8 by @renovate[bot] in https://github.com/uutils/coreutils/pull/10944 +* chore(deps): update rust crate indicatif to v0.18.4 by @renovate[bot] in https://github.com/uutils/coreutils/pull/10936 +* chore(deps): update rust crate memmap2 to v0.9.10 by @renovate[bot] in https://github.com/uutils/coreutils/pull/10953 +* chore(deps): update rust crate clap to v4.5.59 by @renovate[bot] in https://github.com/uutils/coreutils/pull/10985 +* chore(deps): update rust crate zip to v8.1.0 by @renovate[bot] in https://github.com/uutils/coreutils/pull/10990 +* fix(deps): update rust crate keccak to v0.1.6 by @xtqqczze in https://github.com/uutils/coreutils/pull/10988 +* chore(deps): update dawidd6/action-download-artifact action to v15 by @renovate[bot] in https://github.com/uutils/coreutils/pull/11012 +* chore(deps): update rust crate clap to v4.5.60 by @renovate[bot] in https://github.com/uutils/coreutils/pull/11030 +* Bump fuzzer's libc to 0.2.182 by @oech3 in https://github.com/uutils/coreutils/pull/11037 +* fuzz: bump `keccak` from `0.1.5` to `0.1.6` by @cakebaker in https://github.com/uutils/coreutils/pull/11035 +* chore(deps): update rust crate rustix to 1.1.4 by @xtqqczze in https://github.com/uutils/coreutils/pull/11055 +* chore(deps): update rust crate jiff to v0.2.21 by @renovate[bot] in https://github.com/uutils/coreutils/pull/11058 +* chore(deps): update rust crate tempfile to v3.26.0 by @renovate[bot] in https://github.com/uutils/coreutils/pull/11087 +* Bump `num-prime` from `0.4.4` to `0.5.0` by @cakebaker in https://github.com/uutils/coreutils/pull/11069 +* deps: update rust crate futures-util to v0.3.32 by @xtqqczze in https://github.com/uutils/coreutils/pull/11094 +* chore(deps): update dawidd6/action-download-artifact action to v16 by @renovate[bot] in https://github.com/uutils/coreutils/pull/11121 +* chore(deps): update rust crate jiff to v0.2.22 by @renovate[bot] in https://github.com/uutils/coreutils/pull/11164 +* chore(deps): update rust crate zip to v8.2.0 by @renovate[bot] in https://github.com/uutils/coreutils/pull/11184 +* chore(deps): update rust crate quote to v1.0.45 by @renovate[bot] in https://github.com/uutils/coreutils/pull/11188 +* chore(deps): update rust crate jiff to v0.2.23 by @renovate[bot] in https://github.com/uutils/coreutils/pull/11189 +* chore(deps): update github artifact actions (major) by @renovate[bot] in https://github.com/uutils/coreutils/pull/11132 +* deps: replace ahash with foldhash by @xtqqczze in https://github.com/uutils/coreutils/pull/11187 +* deny.toml: remove `signal-hook` from skip list by @cakebaker in https://github.com/uutils/coreutils/pull/10785 + +## Version Management +* prepare release 0.7.0 by @sylvestre in https://github.com/uutils/coreutils/pull/11128 + +## New Contributors +* @my4ng made their first contribution in https://github.com/uutils/coreutils/pull/10673 +* @tuananh made their first contribution in https://github.com/uutils/coreutils/pull/10668 +* @0xMillyByte made their first contribution in https://github.com/uutils/coreutils/pull/10714 +* @NatsuCamellia made their first contribution in https://github.com/uutils/coreutils/pull/10434 +* @nikolalukovic made their first contribution in https://github.com/uutils/coreutils/pull/10529 +* @memark made their first contribution in https://github.com/uutils/coreutils/pull/10848 +* @lanceXwq made their first contribution in https://github.com/uutils/coreutils/pull/10822 +* @denendaden made their first contribution in https://github.com/uutils/coreutils/pull/10711 +* @hlsxx made their first contribution in https://github.com/uutils/coreutils/pull/10827 +* @WhateverAWS made their first contribution in https://github.com/uutils/coreutils/pull/10912 +* @vjardin made their first contribution in https://github.com/uutils/coreutils/pull/10901 +* @yachi made their first contribution in https://github.com/uutils/coreutils/pull/10828 +* @cuiweixie made their first contribution in https://github.com/uutils/coreutils/pull/10973 +* @o1x3 made their first contribution in https://github.com/uutils/coreutils/pull/10974 +* @aguimaraes made their first contribution in https://github.com/uutils/coreutils/pull/11045 +* @victor-prokhorov made their first contribution in https://github.com/uutils/coreutils/pull/10934 +* @RedNhight made their first contribution in https://github.com/uutils/coreutils/pull/11018 +* @anihal made their first contribution in https://github.com/uutils/coreutils/pull/11112 +* @aweinstock314 made their first contribution in https://github.com/uutils/coreutils/pull/11129 +* @YuF-9468 made their first contribution in https://github.com/uutils/coreutils/pull/11159 +* @yotam-medini made their first contribution in https://github.com/uutils/coreutils/pull/11182 +* @akervald made their first contribution in https://github.com/uutils/coreutils/pull/11143 +* @domysu made their first contribution in https://github.com/uutils/coreutils/pull/11206 + +**Full Changelog**: https://github.com/uutils/coreutils/compare/0.6.0...0.7.0 From de7e55ed34a5ea90f336ce2d6a0b7d05d4c83dcc Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Tue, 10 Mar 2026 16:04:47 +0100 Subject: [PATCH 206/264] numfmt: support explicit --unit-separator for suffix parsing --- src/uu/numfmt/src/format.rs | 186 +++++++++++++++++++++++------------ src/uu/numfmt/src/numfmt.rs | 14 +-- src/uu/numfmt/src/options.rs | 2 +- tests/by-util/test_numfmt.rs | 23 +++++ 4 files changed, 149 insertions(+), 76 deletions(-) diff --git a/src/uu/numfmt/src/format.rs b/src/uu/numfmt/src/format.rs index 57abc84588f..d8247bb2324 100644 --- a/src/uu/numfmt/src/format.rs +++ b/src/uu/numfmt/src/format.rs @@ -11,59 +11,6 @@ use uucore::translate; use crate::options::{NumfmtOptions, RoundMethod, TransformOptions}; use crate::units::{DisplayableSuffix, IEC_BASES, RawSuffix, Result, SI_BASES, Suffix, Unit}; -/// Iterate over a line's fields, where each field is a contiguous sequence of -/// non-whitespace, optionally prefixed with one or more characters of leading -/// whitespace. Fields are returned as tuples of `(prefix, field)`. -/// -/// # Examples: -/// -/// ``` -/// let mut fields = uu_numfmt::format::WhitespaceSplitter { s: Some(" 1234 5") }; -/// -/// assert_eq!(Some((" ", "1234")), fields.next()); -/// assert_eq!(Some((" ", "5")), fields.next()); -/// assert_eq!(None, fields.next()); -/// ``` -/// -/// Delimiters are included in the results; `prefix` will be empty only for -/// the first field of the line (including the case where the input line is -/// empty): -/// -/// ``` -/// let mut fields = uu_numfmt::format::WhitespaceSplitter { s: Some("first second") }; -/// -/// assert_eq!(Some(("", "first")), fields.next()); -/// assert_eq!(Some((" ", "second")), fields.next()); -/// -/// let mut fields = uu_numfmt::format::WhitespaceSplitter { s: Some("") }; -/// -/// assert_eq!(Some(("", "")), fields.next()); -/// ``` -pub struct WhitespaceSplitter<'a> { - pub s: Option<&'a str>, -} - -impl<'a> Iterator for WhitespaceSplitter<'a> { - type Item = (&'a str, &'a str); - - /// Yield the next field in the input string as a tuple `(prefix, field)`. - fn next(&mut self) -> Option { - let haystack = self.s?; - - let (prefix, field) = haystack.split_at( - haystack - .find(|c: char| !c.is_whitespace()) - .unwrap_or(haystack.len()), - ); - - let (field, rest) = field.split_at(field.find(char::is_whitespace).unwrap_or(field.len())); - - self.s = if rest.is_empty() { None } else { Some(rest) }; - - Some((prefix, field)) - } -} - fn find_numeric_beginning(s: &str) -> Option<&str> { let mut decimal_point_seen = false; if s.is_empty() { @@ -130,6 +77,9 @@ fn detailed_error_message(s: &str, unit: Unit) -> Option { if valid_part != s && valid_part.parse::().is_ok() { return match s.chars().nth(valid_part.len()) { + Some('+' | '-') => { + Some(translate!("numfmt-error-invalid-number", "input" => s.quote())) + } Some(v) if RawSuffix::try_from(&v).is_ok() => Some( translate!("numfmt-error-rejecting-suffix", "number" => valid_part, "suffix" => s[valid_part.len()..]), ), @@ -146,7 +96,12 @@ fn detailed_error_message(s: &str, unit: Unit) -> Option { None } -fn parse_suffix(s: &str, unit: Unit, max_whitespace: usize) -> Result<(f64, Option)> { +fn parse_suffix( + s: &str, + unit: Unit, + unit_separator: &str, + explicit_unit_separator: bool, +) -> Result<(f64, Option)> { let trimmed = s.trim_end(); if trimmed.is_empty() { return Err(translate!("numfmt-error-invalid-number-empty")); @@ -185,23 +140,116 @@ fn parse_suffix(s: &str, unit: Unit, max_whitespace: usize) -> Result<(f64, Opti }; let number_part = &trimmed[..trimmed.len() - suffix_len]; - let number_trimmed = number_part.trim_end(); - // Validate whitespace between number and suffix if suffix.is_some() { - let whitespace = number_part.len() - number_trimmed.len(); - if whitespace > max_whitespace { - return Err(translate!("numfmt-error-invalid-suffix", "input" => s.quote())); - } + let separator_len = if explicit_unit_separator { + if number_part.ends_with(unit_separator) { + unit_separator.len() + } else if unit_separator.is_empty() { + 0 + } else { + return Err(translate!("numfmt-error-invalid-suffix", "input" => s.quote())); + } + } else { + let number_trimmed = number_part.trim_end(); + let whitespace = number_part.len() - number_trimmed.len(); + if whitespace > 1 { + return Err(translate!("numfmt-error-invalid-suffix", "input" => s.quote())); + } + whitespace + }; + + let number = number_part[..number_part.len() - separator_len] + .parse::() + .map_err(|_| translate!("numfmt-error-invalid-number", "input" => s.quote()))?; + + return Ok((number, suffix)); } - let number = number_trimmed + let number = number_part .parse::() .map_err(|_| translate!("numfmt-error-invalid-number", "input" => s.quote()))?; Ok((number, suffix)) } +fn next_field_index(s: &str) -> usize { + s.find(char::is_whitespace).unwrap_or(s.len()) +} + +fn split_next_field<'a>(s: &'a str) -> (&'a str, &'a str, &'a str) { + let prefix_len = s.find(|c: char| !c.is_whitespace()).unwrap_or(s.len()); + let prefix = &s[..prefix_len]; + let field_start = prefix_len; + let field_len = next_field_index(&s[field_start..]); + let field_end = field_start + field_len; + let field = &s[field_start..field_end]; + let rest = &s[field_end..]; + (prefix, field, rest) +} + +/// When an explicit whitespace unit separator is set (e.g. `--unit-separator=" "`), +/// a suffix like "K" may appear as a separate whitespace-delimited field. Detect +/// this case so the caller can merge the suffix back into the preceding number field. +fn split_mergeable_suffix<'a>(s: &'a str, options: &NumfmtOptions) -> Option<(&'a str, &'a str)> { + if !options.explicit_unit_separator + || options.unit_separator.is_empty() + || !options.unit_separator.chars().all(char::is_whitespace) + { + return None; + } + + if !s.starts_with(&options.unit_separator) { + return None; + } + + let (prefix, field, _) = split_next_field(s); + if prefix != options.unit_separator { + return None; + } + + match field.len() { + 1 => { + let _ = field.chars().next().filter(|c| RawSuffix::try_from(c).is_ok())?; + } + 2 if field.ends_with('i') => { + let _ = field.chars().next().filter(|c| RawSuffix::try_from(c).is_ok())?; + } + _ => return None, + } + + Some((prefix, field)) +} + +struct WhitespaceSplitter<'a, 'b> { + s: Option<&'a str>, + options: &'b NumfmtOptions, +} + +impl<'a> Iterator for WhitespaceSplitter<'a, '_> { + type Item = (&'a str, &'a str); + + fn next(&mut self) -> Option { + let haystack = self.s?; + let (prefix, field, rest) = split_next_field(haystack); + + if field.is_empty() { + self.s = None; + return Some((prefix, field)); + } + + if let Some((suffix_prefix, suffix_field)) = split_mergeable_suffix(rest, self.options) { + let merged_len = prefix.len() + field.len() + suffix_prefix.len() + suffix_field.len(); + let merged_field = &haystack[prefix.len()..merged_len]; + self.s = Some(&haystack[merged_len..]).filter(|rest| !rest.is_empty()); + return Some((prefix, merged_field)); + } + + self.s = Some(rest).filter(|rest| !rest.is_empty()); + Some((prefix, field)) + } +} + /// Returns the implicit precision of a number, which is the count of digits after the dot. For /// example, 1.23 has an implicit precision of 2. fn parse_implicit_precision(s: &str) -> usize { @@ -252,8 +300,13 @@ fn remove_suffix(i: f64, s: Option, u: Unit) -> Result { } } -fn transform_from(s: &str, opts: &TransformOptions, max_whitespace: usize) -> Result { - let (i, suffix) = parse_suffix(s, opts.from, max_whitespace) +fn transform_from(s: &str, opts: &TransformOptions, options: &NumfmtOptions) -> Result { + let (i, suffix) = parse_suffix( + s, + opts.from, + &options.unit_separator, + options.explicit_unit_separator, + ) .map_err(|original| detailed_error_message(s, opts.from).unwrap_or(original))?; let i = i * (opts.from_unit as f64); @@ -412,7 +465,7 @@ fn format_string( transform_from( source_without_suffix, &options.transform, - options.max_whitespace, + options, )?, &options.transform, options.round, @@ -531,7 +584,10 @@ pub fn write_formatted_with_whitespace( options: &NumfmtOptions, eol: Option, ) -> Result<()> { - for (n, (prefix, field)) in (1..).zip(WhitespaceSplitter { s: Some(s) }) { + for (n, (prefix, field)) in (1..).zip(WhitespaceSplitter { + s: Some(s), + options, + }) { let field_selected = uucore::ranges::contain(&options.fields, n); if field_selected { diff --git a/src/uu/numfmt/src/numfmt.rs b/src/uu/numfmt/src/numfmt.rs index e2e86b8e1b3..a779a7cd554 100644 --- a/src/uu/numfmt/src/numfmt.rs +++ b/src/uu/numfmt/src/numfmt.rs @@ -285,14 +285,8 @@ fn parse_options(args: &ArgMatches) -> Result { .cloned() .unwrap_or_default(); - // Max whitespace between number and suffix: length of separator if provided, default one - let max_whitespace = if args.contains_id(UNIT_SEPARATOR) - && args.value_source(UNIT_SEPARATOR) == Some(ValueSource::CommandLine) - { - unit_separator.len() - } else { - 1 - }; + let explicit_unit_separator = args.contains_id(UNIT_SEPARATOR) + && args.value_source(UNIT_SEPARATOR) == Some(ValueSource::CommandLine); let invalid = InvalidModes::from_str(args.get_one::(INVALID).unwrap()).unwrap(); @@ -309,7 +303,7 @@ fn parse_options(args: &ArgMatches) -> Result { round, suffix, unit_separator, - max_whitespace, + explicit_unit_separator, format, invalid, zero_terminated, @@ -530,7 +524,7 @@ mod tests { round: RoundMethod::Nearest, suffix: None, unit_separator: String::new(), - max_whitespace: 1, + explicit_unit_separator: false, format: FormatOptions::default(), invalid: InvalidModes::Abort, zero_terminated: false, diff --git a/src/uu/numfmt/src/options.rs b/src/uu/numfmt/src/options.rs index f6db4ff9f89..21d6e7ff9ec 100644 --- a/src/uu/numfmt/src/options.rs +++ b/src/uu/numfmt/src/options.rs @@ -55,7 +55,7 @@ pub struct NumfmtOptions { pub round: RoundMethod, pub suffix: Option, pub unit_separator: String, - pub max_whitespace: usize, + pub explicit_unit_separator: bool, pub format: FormatOptions, pub invalid: InvalidModes, pub zero_terminated: bool, diff --git a/tests/by-util/test_numfmt.rs b/tests/by-util/test_numfmt.rs index c9ff617c1e9..e9c9994a574 100644 --- a/tests/by-util/test_numfmt.rs +++ b/tests/by-util/test_numfmt.rs @@ -316,6 +316,14 @@ fn test_should_report_invalid_number_with_interior_junk() { .stderr_is("numfmt: invalid suffix in input: '1x0K'\n"); } +#[test] +fn test_should_report_invalid_number_with_sign_after_decimal() { + new_ucmd!() + .args(&["--", "-0.-1"]) + .fails_with_code(2) + .stderr_is("numfmt: invalid number: '-0.-1'\n"); +} + #[test] fn test_should_skip_leading_space_from_stdin() { new_ucmd!() @@ -1237,6 +1245,21 @@ fn test_empty_delimiter_multi_char_unit_separator() { .stdout_only("1000\n2000000\n3000000000\n"); } +#[test] +fn test_whitespace_mode_parses_custom_unit_separator_inputs() { + new_ucmd!() + .args(&["--from=iec", "--unit-separator=::"]) + .pipe_in("4::K\n") + .succeeds() + .stdout_only("4096\n"); + + new_ucmd!() + .args(&["--from=iec", "--unit-separator=\u{a0}"]) + .pipe_in("4\u{a0}K\n") + .succeeds() + .stdout_only("4096\n"); +} + #[test] fn test_empty_delimiter_whitespace_rejection() { new_ucmd!() From 75a72aef29eb79f4411c192b06729c364ea85910 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Tue, 10 Mar 2026 16:07:19 +0100 Subject: [PATCH 207/264] tests: skip unsupported GNU numfmt cases and force symlink refresh Skip GNU numfmt.pl tests not yet supported: unit-sep-22-fail, grp-1, grp-2, delim-7, field-1, field-range-err-{1-4,6-13}, strtod-6, strtod-6.1, leading-4, debug-2, devdebug-{1-7,9-11}, help-1, fmt-err-9, fmt-err-11, fmt-15, ign-err-5, ign-err-m2.2, ign-err-m3.1. Also unconditionally refresh binary symlinks to avoid stale builds. --- util/build-gnu.sh | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/util/build-gnu.sh b/util/build-gnu.sh index bd84e29a86a..e58c2dc34cc 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -95,11 +95,11 @@ else # Use MULTICALL=y for faster build make MULTICALL=y SKIP_UTILS=more for binary in $("${UU_BUILD_DIR}"/coreutils --list) - do [ -e "${UU_BUILD_DIR}/${binary}" ] || ln -vf "${UU_BUILD_DIR}/coreutils" "${UU_BUILD_DIR}/${binary}" + do ln -vf "${UU_BUILD_DIR}/coreutils" "${UU_BUILD_DIR}/${binary}" done ln -vf "${UU_BUILD_DIR}"/deps/libstdbuf.* -t "${UU_BUILD_DIR}" fi -[ -e "${UU_BUILD_DIR}/ginstall" ] || ln -vf "${UU_BUILD_DIR}/install" "${UU_BUILD_DIR}/ginstall" # The GNU tests use ginstall +ln -vf "${UU_BUILD_DIR}/install" "${UU_BUILD_DIR}/ginstall" # The GNU tests use ginstall ## cd "${path_GNU}" && echo "[ pwd:'${PWD}' ]" @@ -192,6 +192,24 @@ sed -i "s|--coreutils-prog=||g" tests/misc/coreutils.sh # Use the system coreutils where the test fails due to error in a util that is not the one being tested sed -i "s|grep '^#define HAVE_CAP 1' \$CONFIG_HEADER > /dev/null|true|" tests/ls/capability.sh +# Keep GNU numfmt coverage focused on the behavior we currently support. +sed -i '/# uutils-numfmt-adjustments start/,/# uutils-numfmt-adjustments end/d' tests/numfmt/numfmt.pl +sed -i '/push @Tests, @Locale_Tests if $locale ne '\''C'\'';/a\ +# uutils-numfmt-adjustments start\ +my %skip_uutils_numfmt_tests = map { $_ => 1 } qw(\ + unit-sep-22-fail grp-1 grp-2 delim-7 field-1\ + field-range-err-1 field-range-err-2 field-range-err-3 field-range-err-4\ + field-range-err-6 field-range-err-7 field-range-err-8 field-range-err-9\ + field-range-err-10 field-range-err-11 field-range-err-12 field-range-err-13\ + strtod-6 strtod-6.1 leading-4 debug-2\ + devdebug-1 devdebug-2 devdebug-3 devdebug-4 devdebug-5 devdebug-6\ + devdebug-7 devdebug-9 devdebug-10 devdebug-11\ + help-1 fmt-err-9 fmt-err-11 fmt-15\ + ign-err-5 ign-err-m2.2 ign-err-m3.1\ +);\ +@Tests = grep { !$skip_uutils_numfmt_tests{$_->[0]} } @Tests;\ +# uutils-numfmt-adjustments end' tests/numfmt/numfmt.pl + # our messages are better sed -i "s|cannot stat 'symlink': Permission denied|not writing through dangling symlink 'symlink'|" tests/cp/fail-perm.sh sed -i "s|cp: target directory 'symlink': Permission denied|cp: 'symlink' is not a directory|" tests/cp/fail-perm.sh From 1467945ea077cab70fef54571dbfae40be7f2ed5 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Tue, 10 Mar 2026 17:21:10 +0100 Subject: [PATCH 208/264] numfmt: add --grouping flag, debug diagnostics, and stricter number validation Fixes GNU numfmt.pl tests: grp-1, grp-2, debug-2, fmt-err-9, fmt-err-11. --- src/uu/numfmt/locales/en-US.ftl | 6 +- src/uu/numfmt/locales/fr-FR.ftl | 2 + src/uu/numfmt/src/format.rs | 100 ++++++++++++++++------ src/uu/numfmt/src/numfmt.rs | 142 +++++++++++++++----------------- src/uu/numfmt/src/options.rs | 2 + tests/by-util/test_numfmt.rs | 61 ++++++++++++++ 6 files changed, 209 insertions(+), 104 deletions(-) diff --git a/src/uu/numfmt/locales/en-US.ftl b/src/uu/numfmt/locales/en-US.ftl index 4e5faf56e7f..c8b51dbe272 100644 --- a/src/uu/numfmt/locales/en-US.ftl +++ b/src/uu/numfmt/locales/en-US.ftl @@ -42,6 +42,7 @@ numfmt-help-field = replace the numbers in these input fields; see FIELDS below numfmt-help-format = use printf style floating-point FORMAT; see FORMAT below for details numfmt-help-from = auto-scale input numbers to UNITs; see UNIT below numfmt-help-from-unit = specify the input unit size +numfmt-help-grouping = use locale-defined grouping of digits, for example 1,000,000 (which means it has no effect in the C/POSIX locale) numfmt-help-to = auto-scale output numbers to UNITs; see UNIT below numfmt-help-to-unit = the output unit size numfmt-help-padding = pad the output to N characters; positive N will right-align; negative N will left-align; padding is ignored if the output is wider than N; the default is to automatically pad if a whitespace is found @@ -57,6 +58,7 @@ numfmt-error-unsupported-unit = Unsupported unit is specified numfmt-error-invalid-unit-size = invalid unit size: { $size } numfmt-error-invalid-padding = invalid padding value { $value } numfmt-error-invalid-header = invalid header value { $value } +numfmt-error-grouping-cannot-be-combined-with-format = --grouping cannot be combined with --format numfmt-error-grouping-cannot-be-combined-with-to = grouping cannot be combined with --to numfmt-error-delimiter-must-be-single-character = the delimiter must be a single character numfmt-error-invalid-number-empty = invalid number: '' @@ -75,7 +77,3 @@ numfmt-error-invalid-format-width-overflow = invalid format '{ $format }' (width numfmt-error-invalid-precision = invalid precision in format '{ $format }' numfmt-error-format-too-many-percent = format '{ $format }' has too many % directives numfmt-error-unknown-invalid-mode = Unknown invalid mode: { $mode } - -# Debug messages -numfmt-debug-no-conversion = no conversion option specified -numfmt-debug-header-ignored = --header ignored with command-line input diff --git a/src/uu/numfmt/locales/fr-FR.ftl b/src/uu/numfmt/locales/fr-FR.ftl index 20bd91db9a9..3a6094eebc4 100644 --- a/src/uu/numfmt/locales/fr-FR.ftl +++ b/src/uu/numfmt/locales/fr-FR.ftl @@ -41,6 +41,7 @@ numfmt-help-field = remplacer les nombres dans ces champs d'entrée ; voir FIELD numfmt-help-format = utiliser le FORMAT à virgule flottante de style printf ; voir FORMAT ci-dessous pour les détails numfmt-help-from = mettre automatiquement à l'échelle les nombres d'entrée vers les UNITÉs ; voir UNIT ci-dessous numfmt-help-from-unit = spécifier la taille de l'unité d'entrée +numfmt-help-grouping = utiliser le groupement des chiffres défini par la locale, par exemple 1 000 000 (ce qui n'a aucun effet dans la locale C/POSIX) numfmt-help-to = mettre automatiquement à l'échelle les nombres de sortie vers les UNITÉs ; voir UNIT ci-dessous numfmt-help-to-unit = la taille de l'unité de sortie numfmt-help-padding = remplir la sortie à N caractères ; N positif alignera à droite ; N négatif alignera à gauche ; le remplissage est ignoré si la sortie est plus large que N ; la valeur par défaut est de remplir automatiquement si un espace est trouvé @@ -55,6 +56,7 @@ numfmt-error-unsupported-unit = Une unité non supportée est spécifiée numfmt-error-invalid-unit-size = taille d'unité invalide : { $size } numfmt-error-invalid-padding = valeur de remplissage invalide { $value } numfmt-error-invalid-header = valeur d'en-tête invalide { $value } +numfmt-error-grouping-cannot-be-combined-with-format = --grouping ne peut pas être combiné avec --format numfmt-error-grouping-cannot-be-combined-with-to = le groupement ne peut pas être combiné avec --to numfmt-error-delimiter-must-be-single-character = le délimiteur doit être un seul caractère numfmt-error-invalid-number-empty = nombre invalide : '' diff --git a/src/uu/numfmt/src/format.rs b/src/uu/numfmt/src/format.rs index d8247bb2324..12f6e9ac58d 100644 --- a/src/uu/numfmt/src/format.rs +++ b/src/uu/numfmt/src/format.rs @@ -6,6 +6,7 @@ // spell-checker:ignore powf use uucore::display::Quotable; +use uucore::i18n::decimal::locale_grouping_separator; use uucore::translate; use crate::options::{NumfmtOptions, RoundMethod, TransformOptions}; @@ -75,6 +76,14 @@ fn detailed_error_message(s: &str, unit: Unit) -> Option { .ok_or(translate!("numfmt-error-invalid-number", "input" => s.quote())) .ok()?; + if number_prefix == "." { + return Some(translate!("numfmt-error-invalid-suffix", "input" => s.quote())); + } + + if number_prefix.ends_with('.') { + return Some(translate!("numfmt-error-invalid-number", "input" => s.quote())); + } + if valid_part != s && valid_part.parse::().is_ok() { return match s.chars().nth(valid_part.len()) { Some('+' | '-') => { @@ -96,6 +105,15 @@ fn detailed_error_message(s: &str, unit: Unit) -> Option { None } +fn parse_number_part(s: &str, input: &str) -> Result { + if s.ends_with('.') { + return Err(translate!("numfmt-error-invalid-number", "input" => input.quote())); + } + + s.parse::() + .map_err(|_| translate!("numfmt-error-invalid-number", "input" => input.quote())) +} + fn parse_suffix( s: &str, unit: Unit, @@ -159,20 +177,48 @@ fn parse_suffix( whitespace }; - let number = number_part[..number_part.len() - separator_len] - .parse::() - .map_err(|_| translate!("numfmt-error-invalid-number", "input" => s.quote()))?; + let number = parse_number_part(&number_part[..number_part.len() - separator_len], s)?; return Ok((number, suffix)); } - let number = number_part - .parse::() - .map_err(|_| translate!("numfmt-error-invalid-number", "input" => s.quote()))?; + let number = parse_number_part(number_part, s)?; Ok((number, suffix)) } +fn apply_grouping(s: &str) -> String { + let grouping_separator = locale_grouping_separator(); + if grouping_separator.is_empty() { + return s.to_string(); + } + + let (sign, rest) = if let Some(rest) = s.strip_prefix('-') { + ("-", rest) + } else { + ("", s) + }; + let (integer, fraction) = rest.split_once('.').map_or((rest, ""), |(i, f)| (i, f)); + if integer.len() < 4 { + return s.to_string(); + } + + let mut grouped_rev = String::with_capacity(s.len() + (integer.len() / 3)); + for (idx, ch) in integer.chars().rev().enumerate() { + if idx > 0 && idx % 3 == 0 { + grouped_rev.push_str(grouping_separator); + } + grouped_rev.push(ch); + } + let grouped_integer: String = grouped_rev.chars().rev().collect(); + + if fraction.is_empty() { + format!("{sign}{grouped_integer}") + } else { + format!("{sign}{grouped_integer}.{fraction}") + } +} + fn next_field_index(s: &str) -> usize { s.find(char::is_whitespace).unwrap_or(s.len()) } @@ -210,10 +256,16 @@ fn split_mergeable_suffix<'a>(s: &'a str, options: &NumfmtOptions) -> Option<(&' match field.len() { 1 => { - let _ = field.chars().next().filter(|c| RawSuffix::try_from(c).is_ok())?; + let _ = field + .chars() + .next() + .filter(|c| RawSuffix::try_from(c).is_ok())?; } 2 if field.ends_with('i') => { - let _ = field.chars().next().filter(|c| RawSuffix::try_from(c).is_ok())?; + let _ = field + .chars() + .next() + .filter(|c| RawSuffix::try_from(c).is_ok())?; } _ => return None, } @@ -307,7 +359,7 @@ fn transform_from(s: &str, opts: &TransformOptions, options: &NumfmtOptions) -> &options.unit_separator, options.explicit_unit_separator, ) - .map_err(|original| detailed_error_message(s, opts.from).unwrap_or(original))?; + .map_err(|original| detailed_error_message(s, opts.from).unwrap_or(original))?; let i = i * (opts.from_unit as f64); remove_suffix(i, suffix, opts.from).map(|n| { @@ -462,11 +514,7 @@ fn format_string( }; let number = transform_to( - transform_from( - source_without_suffix, - &options.transform, - options, - )?, + transform_from(source_without_suffix, &options.transform, options)?, &options.transform, options.round, precision, @@ -474,9 +522,15 @@ fn format_string( )?; // bring back the suffix before applying padding + let grouped_number = if options.grouping { + apply_grouping(&number) + } else { + number + }; + let number_with_suffix = match &options.suffix { - Some(suffix) => format!("{number}{suffix}"), - None => number, + Some(suffix) => format!("{grouped_number}{suffix}"), + None => grouped_number, }; let padding = options @@ -669,7 +723,7 @@ mod tests { #[test] fn test_parse_suffix_q_r_k() { - let result = parse_suffix("1Q", Unit::Auto, 1); + let result = parse_suffix("1Q", Unit::Auto, "", false); assert!(result.is_ok()); let (number, suffix) = result.unwrap(); assert_eq!(number, 1.0); @@ -678,7 +732,7 @@ mod tests { assert_eq!(raw_suffix as i32, RawSuffix::Q as i32); assert!(!with_i); - let result = parse_suffix("2R", Unit::Auto, 1); + let result = parse_suffix("2R", Unit::Auto, "", false); assert!(result.is_ok()); let (number, suffix) = result.unwrap(); assert_eq!(number, 2.0); @@ -687,7 +741,7 @@ mod tests { assert_eq!(raw_suffix as i32, RawSuffix::R as i32); assert!(!with_i); - let result = parse_suffix("3k", Unit::Auto, 1); + let result = parse_suffix("3k", Unit::Auto, "", false); assert!(result.is_ok()); let (number, suffix) = result.unwrap(); assert_eq!(number, 3.0); @@ -696,7 +750,7 @@ mod tests { assert_eq!(raw_suffix as i32, RawSuffix::K as i32); assert!(!with_i); - let result = parse_suffix("4Qi", Unit::Auto, 1); + let result = parse_suffix("4Qi", Unit::Auto, "", false); assert!(result.is_ok()); let (number, suffix) = result.unwrap(); assert_eq!(number, 4.0); @@ -705,7 +759,7 @@ mod tests { assert_eq!(raw_suffix as i32, RawSuffix::Q as i32); assert!(with_i); - let result = parse_suffix("5Ri", Unit::Auto, 1); + let result = parse_suffix("5Ri", Unit::Auto, "", false); assert!(result.is_ok()); let (number, suffix) = result.unwrap(); assert_eq!(number, 5.0); @@ -717,13 +771,13 @@ mod tests { #[test] fn test_parse_suffix_error_messages() { - let result = parse_suffix("foo", Unit::Auto, 1); + let result = parse_suffix("foo", Unit::Auto, "", false); assert!(result.is_err()); let error = result.unwrap_err(); assert!(error.contains("numfmt-error-invalid-number") || error.contains("invalid number")); assert!(!error.contains("invalid suffix")); - let result = parse_suffix("World", Unit::Auto, 1); + let result = parse_suffix("World", Unit::Auto, "", false); assert!(result.is_err()); let error = result.unwrap_err(); assert!(error.contains("numfmt-error-invalid-number") || error.contains("invalid number")); diff --git a/src/uu/numfmt/src/numfmt.rs b/src/uu/numfmt/src/numfmt.rs index a779a7cd554..c0726ac67e1 100644 --- a/src/uu/numfmt/src/numfmt.rs +++ b/src/uu/numfmt/src/numfmt.rs @@ -7,9 +7,9 @@ use crate::errors::NumfmtError; use crate::format::{escape_line, write_formatted_with_delimiter, write_formatted_with_whitespace}; use crate::options::{ DEBUG, DELIMITER, FIELD, FIELD_DEFAULT, FORMAT, FROM, FROM_DEFAULT, FROM_UNIT, - FROM_UNIT_DEFAULT, FormatOptions, HEADER, HEADER_DEFAULT, INVALID, InvalidModes, NUMBER, - NumfmtOptions, PADDING, ROUND, RoundMethod, SUFFIX, TO, TO_DEFAULT, TO_UNIT, TO_UNIT_DEFAULT, - TransformOptions, UNIT_SEPARATOR, ZERO_TERMINATED, + FROM_UNIT_DEFAULT, FormatOptions, GROUPING, HEADER, HEADER_DEFAULT, INVALID, InvalidModes, + NUMBER, NumfmtOptions, PADDING, ROUND, RoundMethod, SUFFIX, TO, TO_DEFAULT, TO_UNIT, + TO_UNIT_DEFAULT, TransformOptions, UNIT_SEPARATOR, ZERO_TERMINATED, }; use crate::units::{Result, Unit}; use clap::{Arg, ArgAction, ArgMatches, Command, builder::ValueParser, parser::ValueSource}; @@ -21,6 +21,7 @@ use units::{IEC_BASES, SI_BASES}; use uucore::display::Quotable; use uucore::error::UResult; +use uucore::i18n::decimal::locale_grouping_separator; use uucore::parser::shortcut_value_parser::ShortcutValueParser; use uucore::ranges::Range; use uucore::{format_usage, os_str_as_bytes, show, translate}; @@ -30,73 +31,28 @@ pub mod format; pub mod options; mod units; -fn handle_args<'a>(args: impl Iterator, options: &NumfmtOptions) -> UResult<()> { - let mut stdout = std::io::stdout().lock(); - let terminator = if options.zero_terminated { 0u8 } else { b'\n' }; - for l in args { - write_line(&mut stdout, l, options, Some(terminator))?; - } - Ok(()) -} - -fn handle_buffer(mut input: R, options: &NumfmtOptions) -> UResult<()> { - let terminator = if options.zero_terminated { 0u8 } else { b'\n' }; - let mut stdout = std::io::stdout().lock(); - let mut buf = Vec::new(); - let mut idx = 0; - - loop { - buf.clear(); - let n = input - .read_until(terminator, &mut buf) - .map_err(|e| NumfmtError::IoError(e.to_string()))?; - if n == 0 { - break; - } - - let has_terminator = buf.last() == Some(&terminator); - let line = if has_terminator { - &buf[..buf.len() - 1] - } else { - &buf[..] - }; - - // Emit the terminator only if the input line had one. - // i.e. if the last line of the input does not end with a newline, we should not add one. - let eol = has_terminator.then_some(terminator); - - if idx < options.header { - stdout.write_all(line)?; - if let Some(t) = eol { - stdout.write_all(&[t])?; - } - } else { - write_line(&mut stdout, line, options, eol)?; - } - - idx += 1; - } - - Ok(()) -} - -fn write_line( +/// Format a single line and write it, handling `--invalid` error modes. +/// +/// Returns `true` if the line contained invalid input (only possible in +/// non-abort modes). +fn format_and_write( writer: &mut W, input_line: &[u8], options: &NumfmtOptions, eol: Option, -) -> UResult<()> { - // Read lines only up to null byte (as GNU does) +) -> UResult { + // GNU truncates at the first embedded null byte. let line = match memchr::memchr(b'\0', input_line) { Some(i) => &input_line[..i], None => input_line, }; + let mut formatted_line = Vec::new(); let handled_line = if options.delimiter.is_some() { - write_formatted_with_delimiter(writer, line, options, eol) + write_formatted_with_delimiter(&mut formatted_line, line, options, eol) } else { // Whitespace mode requires valid UTF-8 match std::str::from_utf8(line) { - Ok(s) => write_formatted_with_whitespace(writer, s, options, eol), + Ok(s) => write_formatted_with_whitespace(&mut formatted_line, s, options, eol), Err(_) => { Err(translate!("numfmt-error-invalid-number", "input" => escape_line(line).quote())) } @@ -121,9 +77,11 @@ fn write_line( if let Some(eol) = eol { writer.write_all(&[eol])?; } + return Ok(true); } - Ok(()) + writer.write_all(&formatted_line)?; + Ok(false) } fn parse_unit(s: &str) -> Result { @@ -252,12 +210,21 @@ fn parse_options(args: &ArgMatches) -> Result { Range::from_list(fields)? }; + let grouping = args.get_flag(GROUPING); let format = match args.get_one::(FORMAT) { Some(s) => s.parse()?, None => FormatOptions::default(), }; - if format.grouping && to != Unit::None { + if grouping && args.contains_id(FORMAT) { + return Err(translate!( + "numfmt-error-grouping-cannot-be-combined-with-format" + )); + } + + let grouping = grouping || format.grouping; + + if grouping && to != Unit::None { return Err(translate!( "numfmt-error-grouping-cannot-be-combined-with-to" )); @@ -303,6 +270,7 @@ fn parse_options(args: &ArgMatches) -> Result { round, suffix, unit_separator, + grouping, explicit_unit_separator, format, invalid, @@ -314,15 +282,14 @@ fn parse_options(args: &ArgMatches) -> Result { fn print_debug_warnings(options: &NumfmtOptions, matches: &ArgMatches) { fn print_warning(msg_key: &str) { let _ = writeln!(stderr(), "numfmt: {}", translate!(msg_key)); - } - - // Warn if no conversion option is specified - // 2>/dev/full does not abort - if options.transform.from == Unit::None - && options.transform.to == Unit::None - && options.padding == 0 - { - print_warning("numfmt-debug-no-conversion"); + let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?; + if options.grouping && locale_grouping_separator().is_empty() { + let _ = writeln!( + stderr(), + "{}: {}", + util_name(), + translate!("numfmt-debug-grouping-no-effect") + ); } // Warn if --header is used with command-line input @@ -333,7 +300,17 @@ fn print_debug_warnings(options: &NumfmtOptions, matches: &ArgMatches) { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?; + // GNU numfmt accepts both --debug and ---debug (triple dash); normalize for clap. + let normalized_args: Vec = args + .map(|arg| { + if arg == "---debug" { + OsString::from("--debug") + } else { + arg + } + }) + .collect(); + let matches = uucore::clap_localization::handle_clap_result(uu_app(), normalized_args)?; let options = parse_options(&matches).map_err(NumfmtError::IllegalArgument)?; @@ -346,19 +323,23 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { .map(|s| os_str_as_bytes(s).map_err(|e| e.to_string())) .collect::, _>>() .map_err(NumfmtError::IllegalArgument)?; - handle_args(byte_args.into_iter(), &options) - } else { - let stdin = std::io::stdin(); - let mut locked_stdin = stdin.lock(); - handle_buffer(&mut locked_stdin, &options) - }; match result { Err(e) => { std::io::stdout().flush().expect("error flushing stdout"); Err(e) } - _ => Ok(()), + Ok(saw_invalid) => { + if options.debug && saw_invalid { + let _ = writeln!( + stderr(), + "{}: {}", + util_name(), + translate!("numfmt-debug-failed-to-convert") + ); + } + Ok(()) + } } } @@ -377,6 +358,12 @@ pub fn uu_app() -> Command { .help(translate!("numfmt-help-debug")) .action(ArgAction::SetTrue), ) + .arg( + Arg::new(GROUPING) + .long(GROUPING) + .help(translate!("numfmt-help-grouping")) + .action(ArgAction::SetTrue), + ) .arg( Arg::new(DELIMITER) .short('d') @@ -524,6 +511,7 @@ mod tests { round: RoundMethod::Nearest, suffix: None, unit_separator: String::new(), + grouping: false, explicit_unit_separator: false, format: FormatOptions::default(), invalid: InvalidModes::Abort, diff --git a/src/uu/numfmt/src/options.rs b/src/uu/numfmt/src/options.rs index 21d6e7ff9ec..ced2d4b6493 100644 --- a/src/uu/numfmt/src/options.rs +++ b/src/uu/numfmt/src/options.rs @@ -17,6 +17,7 @@ pub const FROM: &str = "from"; pub const FROM_DEFAULT: &str = "none"; pub const FROM_UNIT: &str = "from-unit"; pub const FROM_UNIT_DEFAULT: &str = "1"; +pub const GROUPING: &str = "grouping"; pub const HEADER: &str = "header"; pub const HEADER_DEFAULT: &str = "1"; pub const INVALID: &str = "invalid"; @@ -55,6 +56,7 @@ pub struct NumfmtOptions { pub round: RoundMethod, pub suffix: Option, pub unit_separator: String, + pub grouping: bool, pub explicit_unit_separator: bool, pub format: FormatOptions, pub invalid: InvalidModes, diff --git a/tests/by-util/test_numfmt.rs b/tests/by-util/test_numfmt.rs index e9c9994a574..f4af8f4f131 100644 --- a/tests/by-util/test_numfmt.rs +++ b/tests/by-util/test_numfmt.rs @@ -1113,6 +1113,14 @@ fn test_format_grouping_conflicts_with_to_option() { .stderr_contains("grouping cannot be combined with --to"); } +#[test] +fn test_grouping_conflicts_with_format_option() { + new_ucmd!() + .args(&["--format=%f", "--grouping"]) + .fails_with_code(1) + .stderr_contains("--grouping cannot be combined with --format"); +} + #[test] fn test_zero_terminated_command_line_args() { new_ucmd!() @@ -1208,6 +1216,59 @@ fn test_debug_warnings() { .succeeds() .stdout_is("4.0K\n") .stderr_is("numfmt: --header ignored with command-line input\n"); + + new_ucmd!() + .env("LC_ALL", "C") + .args(&["--debug", "--grouping", "--from=si", "4.0K"]) + .succeeds() + .stdout_is("4000\n") + .stderr_is("numfmt: grouping has no effect in this locale\n"); +} + +#[test] +fn test_debug_reports_failed_conversions_summary() { + new_ucmd!() + .args(&[ + "--invalid=fail", + "--debug", + "--to=si", + "1000", + "Foo", + "3000", + ]) + .fails_with_code(2) + .stdout_is("1.0k\nFoo\n3.0k\n") + .stderr_is( + "numfmt: invalid number: 'Foo'\nnumfmt: failed to convert some of the input numbers\n", + ); +} + +#[test] +fn test_invalid_fail_with_fields_does_not_duplicate_output() { + new_ucmd!() + .args(&["--invalid=fail", "--field=2", "--from=si", "--to=iec"]) + .pipe_in("A 1K x\nB Foo y\nC 3G z\n") + .fails_with_code(2) + .stdout_is("A 1000 x\nB Foo y\nC 2.8G z\n") + .stderr_is("numfmt: invalid number: 'Foo'\n"); +} + +#[test] +fn test_rejects_malformed_number_forms() { + new_ucmd!() + .args(&["--from=si", "12.K"]) + .fails_with_code(2) + .stderr_contains("invalid number: '12.K'"); + + new_ucmd!() + .args(&["--from=si", "--delimiter=,", "12. 2"]) + .fails_with_code(2) + .stderr_contains("invalid number: '12. 2'"); + + new_ucmd!() + .arg("..1") + .fails_with_code(2) + .stderr_contains("invalid suffix in input: '..1'"); } #[test] From 03c75185d1e84cb316793f4f85a0cc646eb4598e Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Tue, 10 Mar 2026 17:21:20 +0100 Subject: [PATCH 209/264] numfmt: buffer output in non-abort invalid modes and re-enable GNU tests Buffer formatted output so that --invalid=fail does not duplicate lines on error, and --invalid=abort streams directly for partial output. Fixes GNU numfmt.pl test: field-3. Re-enables the full numfmt.pl test suite by removing the skip list added earlier in the stack. --- src/uu/numfmt/src/format.rs | 4 ++-- src/uu/numfmt/src/numfmt.rs | 34 +++++++++++++++++++++++++--------- tests/by-util/test_numfmt.rs | 9 +++++++++ util/build-gnu.sh | 18 ------------------ 4 files changed, 36 insertions(+), 29 deletions(-) diff --git a/src/uu/numfmt/src/format.rs b/src/uu/numfmt/src/format.rs index 12f6e9ac58d..d2b5aed5c4d 100644 --- a/src/uu/numfmt/src/format.rs +++ b/src/uu/numfmt/src/format.rs @@ -596,7 +596,7 @@ fn split_bytes<'a>(input: &'a [u8], delim: &'a [u8]) -> impl Iterator( +pub fn write_formatted_with_delimiter( writer: &mut W, input: &[u8], options: &NumfmtOptions, @@ -632,7 +632,7 @@ pub fn write_formatted_with_delimiter( Ok(()) } -pub fn write_formatted_with_whitespace( +pub fn write_formatted_with_whitespace( writer: &mut W, s: &str, options: &NumfmtOptions, diff --git a/src/uu/numfmt/src/numfmt.rs b/src/uu/numfmt/src/numfmt.rs index c0726ac67e1..63908c302ff 100644 --- a/src/uu/numfmt/src/numfmt.rs +++ b/src/uu/numfmt/src/numfmt.rs @@ -20,6 +20,7 @@ use std::str::FromStr; use units::{IEC_BASES, SI_BASES}; use uucore::display::Quotable; use uucore::error::UResult; +use uucore::util_name; use uucore::i18n::decimal::locale_grouping_separator; use uucore::parser::shortcut_value_parser::ShortcutValueParser; @@ -46,15 +47,28 @@ fn format_and_write( Some(i) => &input_line[..i], None => input_line, }; + + // In non-abort modes we buffer the formatted output so that on error we + // can emit the original line instead. + let buffer_output = !matches!(options.invalid, InvalidModes::Abort); let mut formatted_line = Vec::new(); - let handled_line = if options.delimiter.is_some() { - write_formatted_with_delimiter(&mut formatted_line, line, options, eol) - } else { - // Whitespace mode requires valid UTF-8 - match std::str::from_utf8(line) { - Ok(s) => write_formatted_with_whitespace(&mut formatted_line, s, options, eol), - Err(_) => { - Err(translate!("numfmt-error-invalid-number", "input" => escape_line(line).quote())) + let handled_line = { + let output: &mut dyn std::io::Write = if buffer_output { + &mut formatted_line + } else { + writer + }; + + if options.delimiter.is_some() { + write_formatted_with_delimiter(output, line, options, eol) + } else { + // Whitespace mode requires valid UTF-8 + match std::str::from_utf8(line) { + Ok(s) => write_formatted_with_whitespace(output, s, options, eol), + Err(_) => Err(translate!( + "numfmt-error-invalid-number", + "input" => escape_line(line).quote() + )), } } }; @@ -80,7 +94,9 @@ fn format_and_write( return Ok(true); } - writer.write_all(&formatted_line)?; + if buffer_output { + writer.write_all(&formatted_line)?; + } Ok(false) } diff --git a/tests/by-util/test_numfmt.rs b/tests/by-util/test_numfmt.rs index f4af8f4f131..4ffc258d799 100644 --- a/tests/by-util/test_numfmt.rs +++ b/tests/by-util/test_numfmt.rs @@ -1253,6 +1253,15 @@ fn test_invalid_fail_with_fields_does_not_duplicate_output() { .stderr_is("numfmt: invalid number: 'Foo'\n"); } +#[test] +fn test_abort_with_fields_preserves_partial_output() { + new_ucmd!() + .args(&["--field=3", "--from=auto", "Hello 40M World 90G"]) + .fails_with_code(2) + .stdout_is("Hello 40M ") + .stderr_is("numfmt: invalid number: 'World'\n"); +} + #[test] fn test_rejects_malformed_number_forms() { new_ucmd!() diff --git a/util/build-gnu.sh b/util/build-gnu.sh index e58c2dc34cc..6c5cb6145f6 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -192,24 +192,6 @@ sed -i "s|--coreutils-prog=||g" tests/misc/coreutils.sh # Use the system coreutils where the test fails due to error in a util that is not the one being tested sed -i "s|grep '^#define HAVE_CAP 1' \$CONFIG_HEADER > /dev/null|true|" tests/ls/capability.sh -# Keep GNU numfmt coverage focused on the behavior we currently support. -sed -i '/# uutils-numfmt-adjustments start/,/# uutils-numfmt-adjustments end/d' tests/numfmt/numfmt.pl -sed -i '/push @Tests, @Locale_Tests if $locale ne '\''C'\'';/a\ -# uutils-numfmt-adjustments start\ -my %skip_uutils_numfmt_tests = map { $_ => 1 } qw(\ - unit-sep-22-fail grp-1 grp-2 delim-7 field-1\ - field-range-err-1 field-range-err-2 field-range-err-3 field-range-err-4\ - field-range-err-6 field-range-err-7 field-range-err-8 field-range-err-9\ - field-range-err-10 field-range-err-11 field-range-err-12 field-range-err-13\ - strtod-6 strtod-6.1 leading-4 debug-2\ - devdebug-1 devdebug-2 devdebug-3 devdebug-4 devdebug-5 devdebug-6\ - devdebug-7 devdebug-9 devdebug-10 devdebug-11\ - help-1 fmt-err-9 fmt-err-11 fmt-15\ - ign-err-5 ign-err-m2.2 ign-err-m3.1\ -);\ -@Tests = grep { !$skip_uutils_numfmt_tests{$_->[0]} } @Tests;\ -# uutils-numfmt-adjustments end' tests/numfmt/numfmt.pl - # our messages are better sed -i "s|cannot stat 'symlink': Permission denied|not writing through dangling symlink 'symlink'|" tests/cp/fail-perm.sh sed -i "s|cp: target directory 'symlink': Permission denied|cp: 'symlink' is not a directory|" tests/cp/fail-perm.sh From 854a1649a80a025fcef00f543a2d1164b341e5c8 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Thu, 19 Mar 2026 08:38:17 +0100 Subject: [PATCH 210/264] numfmt: replace double-reverse with forward pass in apply_grouping Build the grouped string in a single forward pass using chunk iteration instead of reversing, inserting separators, then reversing again. This avoids an extra String allocation and collection. --- src/uu/numfmt/src/format.rs | 37 ++++++++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/src/uu/numfmt/src/format.rs b/src/uu/numfmt/src/format.rs index d2b5aed5c4d..70dfdf21928 100644 --- a/src/uu/numfmt/src/format.rs +++ b/src/uu/numfmt/src/format.rs @@ -203,20 +203,35 @@ fn apply_grouping(s: &str) -> String { return s.to_string(); } - let mut grouped_rev = String::with_capacity(s.len() + (integer.len() / 3)); - for (idx, ch) in integer.chars().rev().enumerate() { - if idx > 0 && idx % 3 == 0 { - grouped_rev.push_str(grouping_separator); - } - grouped_rev.push(ch); + let sep_len = grouping_separator.len(); + let num_seps = (integer.len() - 1) / 3; + let mut grouped = String::with_capacity( + sign.len() + + integer.len() + + num_seps * sep_len + + if fraction.is_empty() { + 0 + } else { + 1 + fraction.len() + }, + ); + grouped.push_str(sign); + + let first_group = integer.len() % 3; + let first_group = if first_group == 0 { 3 } else { first_group }; + grouped.push_str(&integer[..first_group]); + for chunk in integer.as_bytes()[first_group..].chunks(3) { + grouped.push_str(grouping_separator); + // SAFETY: integer is known to be valid UTF-8 ASCII digits + grouped.push_str(std::str::from_utf8(chunk).unwrap()); } - let grouped_integer: String = grouped_rev.chars().rev().collect(); - if fraction.is_empty() { - format!("{sign}{grouped_integer}") - } else { - format!("{sign}{grouped_integer}.{fraction}") + if !fraction.is_empty() { + grouped.push('.'); + grouped.push_str(fraction); } + + grouped } fn next_field_index(s: &str) -> usize { From e73510dae3270ec88ebf5d7aadc7ee321ef82810 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Thu, 19 Mar 2026 08:38:32 +0100 Subject: [PATCH 211/264] numfmt: inline next_field_index into split_next_field Remove the trivial single-use helper and simplify split_next_field into a compact 4-line function. --- src/uu/numfmt/src/format.rs | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/uu/numfmt/src/format.rs b/src/uu/numfmt/src/format.rs index 70dfdf21928..98722ea7678 100644 --- a/src/uu/numfmt/src/format.rs +++ b/src/uu/numfmt/src/format.rs @@ -234,19 +234,12 @@ fn apply_grouping(s: &str) -> String { grouped } -fn next_field_index(s: &str) -> usize { - s.find(char::is_whitespace).unwrap_or(s.len()) -} - -fn split_next_field<'a>(s: &'a str) -> (&'a str, &'a str, &'a str) { +fn split_next_field(s: &str) -> (&str, &str, &str) { let prefix_len = s.find(|c: char| !c.is_whitespace()).unwrap_or(s.len()); - let prefix = &s[..prefix_len]; - let field_start = prefix_len; - let field_len = next_field_index(&s[field_start..]); - let field_end = field_start + field_len; - let field = &s[field_start..field_end]; - let rest = &s[field_end..]; - (prefix, field, rest) + let field_end = s[prefix_len..] + .find(char::is_whitespace) + .map_or(s.len(), |i| prefix_len + i); + (&s[..prefix_len], &s[prefix_len..field_end], &s[field_end..]) } /// When an explicit whitespace unit separator is set (e.g. `--unit-separator=" "`), From 669d4a0236c1e3f9a5c2237a5fc8b31048b4049a Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Thu, 19 Mar 2026 08:38:48 +0100 Subject: [PATCH 212/264] numfmt: deduplicate suffix validation in split_mergeable_suffix The two match arms for length 1 and length 2 had identical RawSuffix::try_from checks. Unify into a single check after a boolean guard. --- src/uu/numfmt/src/format.rs | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/src/uu/numfmt/src/format.rs b/src/uu/numfmt/src/format.rs index 98722ea7678..e9b7336bc96 100644 --- a/src/uu/numfmt/src/format.rs +++ b/src/uu/numfmt/src/format.rs @@ -262,21 +262,18 @@ fn split_mergeable_suffix<'a>(s: &'a str, options: &NumfmtOptions) -> Option<(&' return None; } - match field.len() { - 1 => { - let _ = field - .chars() - .next() - .filter(|c| RawSuffix::try_from(c).is_ok())?; - } - 2 if field.ends_with('i') => { - let _ = field - .chars() - .next() - .filter(|c| RawSuffix::try_from(c).is_ok())?; - } - _ => return None, + let is_suffix = match field.len() { + 1 => true, + 2 => field.ends_with('i'), + _ => false, + }; + if !is_suffix { + return None; } + field + .chars() + .next() + .filter(|c| RawSuffix::try_from(c).is_ok())?; Some((prefix, field)) } From fcdd41b0c618b714f15a1f2995b9996208b2025f Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Thu, 19 Mar 2026 08:39:10 +0100 Subject: [PATCH 213/264] numfmt: simplify write_line buffering logic Flatten the nested block scope by moving the output trait object and format dispatch to the same level, reducing indentation. --- src/uu/numfmt/src/numfmt.rs | 32 ++++++++++++-------------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/src/uu/numfmt/src/numfmt.rs b/src/uu/numfmt/src/numfmt.rs index 63908c302ff..fef2f6608a8 100644 --- a/src/uu/numfmt/src/numfmt.rs +++ b/src/uu/numfmt/src/numfmt.rs @@ -51,25 +51,18 @@ fn format_and_write( // In non-abort modes we buffer the formatted output so that on error we // can emit the original line instead. let buffer_output = !matches!(options.invalid, InvalidModes::Abort); - let mut formatted_line = Vec::new(); - let handled_line = { - let output: &mut dyn std::io::Write = if buffer_output { - &mut formatted_line - } else { - writer - }; + let mut buf = Vec::new(); + let dest: &mut dyn std::io::Write = if buffer_output { &mut buf } else { writer }; - if options.delimiter.is_some() { - write_formatted_with_delimiter(output, line, options, eol) - } else { - // Whitespace mode requires valid UTF-8 - match std::str::from_utf8(line) { - Ok(s) => write_formatted_with_whitespace(output, s, options, eol), - Err(_) => Err(translate!( - "numfmt-error-invalid-number", - "input" => escape_line(line).quote() - )), - } + let result = if options.delimiter.is_some() { + write_formatted_with_delimiter(dest, line, options, eol) + } else { + match std::str::from_utf8(line) { + Ok(s) => write_formatted_with_whitespace(dest, s, options, eol), + Err(_) => Err(translate!( + "numfmt-error-invalid-number", + "input" => escape_line(line).quote() + )), } }; @@ -87,7 +80,6 @@ fn format_and_write( InvalidModes::Ignore => {} } writer.write_all(input_line)?; - if let Some(eol) = eol { writer.write_all(&[eol])?; } @@ -95,7 +87,7 @@ fn format_and_write( } if buffer_output { - writer.write_all(&formatted_line)?; + writer.write_all(&buf)?; } Ok(false) } From 70cb8504eb4991de0a5506d5fdf746f5e27ed885 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Thu, 19 Mar 2026 08:39:36 +0100 Subject: [PATCH 214/264] numfmt: use consistent debug warning format Use the existing print_warning helper for the grouping-no-effect message instead of a one-off writeln with util_name(). Use hardcoded "numfmt:" prefix for the failed-to-convert message to match the other debug warnings. Remove now-unused util_name import. --- src/uu/numfmt/src/numfmt.rs | 38 +------------------------------------ 1 file changed, 1 insertion(+), 37 deletions(-) diff --git a/src/uu/numfmt/src/numfmt.rs b/src/uu/numfmt/src/numfmt.rs index fef2f6608a8..6f002c1a698 100644 --- a/src/uu/numfmt/src/numfmt.rs +++ b/src/uu/numfmt/src/numfmt.rs @@ -20,8 +20,6 @@ use std::str::FromStr; use units::{IEC_BASES, SI_BASES}; use uucore::display::Quotable; use uucore::error::UResult; -use uucore::util_name; - use uucore::i18n::decimal::locale_grouping_separator; use uucore::parser::shortcut_value_parser::ShortcutValueParser; use uucore::ranges::Range; @@ -287,39 +285,6 @@ fn parse_options(args: &ArgMatches) -> Result { }) } -fn print_debug_warnings(options: &NumfmtOptions, matches: &ArgMatches) { - fn print_warning(msg_key: &str) { - let _ = writeln!(stderr(), "numfmt: {}", translate!(msg_key)); - let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?; - if options.grouping && locale_grouping_separator().is_empty() { - let _ = writeln!( - stderr(), - "{}: {}", - util_name(), - translate!("numfmt-debug-grouping-no-effect") - ); - } - - // Warn if --header is used with command-line input - if options.header > 0 && matches.get_many::(NUMBER).is_some() { - print_warning("numfmt-debug-header-ignored"); - } -} - -#[uucore::main] -pub fn uumain(args: impl uucore::Args) -> UResult<()> { - // GNU numfmt accepts both --debug and ---debug (triple dash); normalize for clap. - let normalized_args: Vec = args - .map(|arg| { - if arg == "---debug" { - OsString::from("--debug") - } else { - arg - } - }) - .collect(); - let matches = uucore::clap_localization::handle_clap_result(uu_app(), normalized_args)?; - let options = parse_options(&matches).map_err(NumfmtError::IllegalArgument)?; if options.debug { @@ -341,8 +306,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { if options.debug && saw_invalid { let _ = writeln!( stderr(), - "{}: {}", - util_name(), + "numfmt: {}", translate!("numfmt-debug-failed-to-convert") ); } From 2df23251b22f0dede82be23c21a614bafce1fee3 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Thu, 19 Mar 2026 08:55:12 +0100 Subject: [PATCH 215/264] numfmt: adjust the import --- src/uu/numfmt/Cargo.toml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/uu/numfmt/Cargo.toml b/src/uu/numfmt/Cargo.toml index e7ce4f9c951..f17737dd4c9 100644 --- a/src/uu/numfmt/Cargo.toml +++ b/src/uu/numfmt/Cargo.toml @@ -20,7 +20,12 @@ path = "src/numfmt.rs" [dependencies] clap = { workspace = true } -uucore = { workspace = true, features = ["parser", "ranges"] } +uucore = { workspace = true, features = [ + "parser", + "ranges", + "i18n-common", + "i18n-decimal", +] } thiserror = { workspace = true } fluent = { workspace = true } memchr = { workspace = true } From 668a1906f26d152ef7768c64a7e4a946238d92cc Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Thu, 19 Mar 2026 19:26:37 +0100 Subject: [PATCH 216/264] numfmt: fix clippy errors, remove ---debug alias, rename write_line to format_and_write --- src/uu/numfmt/locales/en-US.ftl | 6 ++ src/uu/numfmt/locales/fr-FR.ftl | 7 +++ src/uu/numfmt/src/format.rs | 2 +- src/uu/numfmt/src/numfmt.rs | 105 ++++++++++++++++++++++++++++++-- 4 files changed, 114 insertions(+), 6 deletions(-) diff --git a/src/uu/numfmt/locales/en-US.ftl b/src/uu/numfmt/locales/en-US.ftl index c8b51dbe272..c0e2c5722e7 100644 --- a/src/uu/numfmt/locales/en-US.ftl +++ b/src/uu/numfmt/locales/en-US.ftl @@ -77,3 +77,9 @@ numfmt-error-invalid-format-width-overflow = invalid format '{ $format }' (width numfmt-error-invalid-precision = invalid precision in format '{ $format }' numfmt-error-format-too-many-percent = format '{ $format }' has too many % directives numfmt-error-unknown-invalid-mode = Unknown invalid mode: { $mode } + +# Debug messages +numfmt-debug-no-conversion = no conversion option specified +numfmt-debug-grouping-no-effect = grouping has no effect in this locale +numfmt-debug-failed-to-convert = failed to convert some of the input numbers +numfmt-debug-header-ignored = --header ignored with command-line input diff --git a/src/uu/numfmt/locales/fr-FR.ftl b/src/uu/numfmt/locales/fr-FR.ftl index 3a6094eebc4..c6eb9b8ab67 100644 --- a/src/uu/numfmt/locales/fr-FR.ftl +++ b/src/uu/numfmt/locales/fr-FR.ftl @@ -36,6 +36,7 @@ numfmt-after-help = Options d'UNITÉ : Une précision optionnelle (%.1f) remplacera la précision déterminée par l'entrée. # Messages d'aide +numfmt-help-debug = afficher des avertissements sur les entrées invalides numfmt-help-delimiter = utiliser X au lieu d'espaces pour le délimiteur de champ numfmt-help-field = remplacer les nombres dans ces champs d'entrée ; voir FIELDS ci-dessous numfmt-help-format = utiliser le FORMAT à virgule flottante de style printf ; voir FORMAT ci-dessous pour les détails @@ -75,3 +76,9 @@ numfmt-error-invalid-format-width-overflow = format invalide '{ $format }' (déb numfmt-error-invalid-precision = précision invalide dans le format '{ $format }' numfmt-error-format-too-many-percent = le format '{ $format }' a trop de directives % numfmt-error-unknown-invalid-mode = Mode invalide inconnu : { $mode } + +# Messages de débogage +numfmt-debug-no-conversion = aucune option de conversion spécifiée +numfmt-debug-grouping-no-effect = le groupement n'a aucun effet dans cette locale +numfmt-debug-failed-to-convert = échec de conversion d'une partie des nombres en entrée +numfmt-debug-header-ignored = --header ignoré avec une entrée en ligne de commande diff --git a/src/uu/numfmt/src/format.rs b/src/uu/numfmt/src/format.rs index e9b7336bc96..2d5dcf7f155 100644 --- a/src/uu/numfmt/src/format.rs +++ b/src/uu/numfmt/src/format.rs @@ -3,7 +3,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore powf +// spell-checker:ignore powf seps use uucore::display::Quotable; use uucore::i18n::decimal::locale_grouping_separator; diff --git a/src/uu/numfmt/src/numfmt.rs b/src/uu/numfmt/src/numfmt.rs index 6f002c1a698..3fc22b8ab94 100644 --- a/src/uu/numfmt/src/numfmt.rs +++ b/src/uu/numfmt/src/numfmt.rs @@ -2,6 +2,7 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +// spell-checker:ignore behavior use crate::errors::NumfmtError; use crate::format::{escape_line, write_formatted_with_delimiter, write_formatted_with_whitespace}; @@ -64,19 +65,20 @@ fn format_and_write( } }; - if let Err(error_message) = handled_line { + if let Err(msg) = result { match options.invalid { InvalidModes::Abort => { - return Err(Box::new(NumfmtError::FormattingError(error_message))); + return Err(Box::new(NumfmtError::FormattingError(msg))); } InvalidModes::Fail => { - show!(NumfmtError::FormattingError(error_message)); + show!(NumfmtError::FormattingError(msg)); } InvalidModes::Warn => { - let _ = writeln!(stderr(), "numfmt: {error_message}"); + let _ = writeln!(stderr(), "numfmt: {msg}"); } InvalidModes::Ignore => {} } + // On error, echo the original line unchanged. writer.write_all(input_line)?; if let Some(eol) = eol { writer.write_all(&[eol])?; @@ -90,6 +92,64 @@ fn format_and_write( Ok(false) } +/// Process command-line number arguments. +/// +/// Returns `true` if any line contained invalid input. +fn handle_args<'a>(args: impl Iterator, options: &NumfmtOptions) -> UResult { + let mut stdout = std::io::stdout().lock(); + let terminator = if options.zero_terminated { 0u8 } else { b'\n' }; + let mut saw_invalid = false; + for l in args { + saw_invalid |= format_and_write(&mut stdout, l, options, Some(terminator))?; + } + Ok(saw_invalid) +} + +/// Process lines read from stdin. +/// +/// Returns `true` if any line contained invalid input. +fn handle_buffer(mut input: R, options: &NumfmtOptions) -> UResult { + let terminator = if options.zero_terminated { 0u8 } else { b'\n' }; + let mut stdout = std::io::stdout().lock(); + let mut buf = Vec::new(); + let mut line_idx = 0; + let mut saw_invalid = false; + + loop { + buf.clear(); + let n = input + .read_until(terminator, &mut buf) + .map_err(|e| NumfmtError::IoError(e.to_string()))?; + if n == 0 { + break; + } + + let has_terminator = buf.last() == Some(&terminator); + let line = if has_terminator { + &buf[..buf.len() - 1] + } else { + &buf[..] + }; + // Emit the terminator only when the input line had one (preserve + // missing final newline). + let eol = has_terminator.then_some(terminator); + + if line_idx < options.header { + // Pass header lines through unchanged. + stdout.write_all(line)?; + if let Some(t) = eol { + stdout.write_all(&[t])?; + } + } else { + saw_invalid |= format_and_write(&mut stdout, line, options, eol)?; + } + + line_idx += 1; + } + + Ok(saw_invalid) +} + fn parse_unit(s: &str) -> Result { match s { "auto" => Ok(Unit::Auto), @@ -285,6 +345,34 @@ fn parse_options(args: &ArgMatches) -> Result { }) } +fn print_debug_warnings(options: &NumfmtOptions, matches: &ArgMatches) { + fn print_warning(msg_key: &str) { + let _ = writeln!(stderr(), "numfmt: {}", translate!(msg_key)); + } + + // Warn if no conversion option is specified + // 2>/dev/full does not abort + if options.transform.from == Unit::None + && options.transform.to == Unit::None + && options.padding == 0 + && !options.grouping + { + print_warning("numfmt-debug-no-conversion"); + } + + if options.grouping && locale_grouping_separator().is_empty() { + print_warning("numfmt-debug-grouping-no-effect"); + } + + // Warn if --header is used with command-line input + if options.header > 0 && matches.get_many::(NUMBER).is_some() { + print_warning("numfmt-debug-header-ignored"); + } +} + +#[uucore::main] +pub fn uumain(args: impl uucore::Args) -> UResult<()> { + let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?; let options = parse_options(&matches).map_err(NumfmtError::IllegalArgument)?; if options.debug { @@ -296,10 +384,17 @@ fn parse_options(args: &ArgMatches) -> Result { .map(|s| os_str_as_bytes(s).map_err(|e| e.to_string())) .collect::, _>>() .map_err(NumfmtError::IllegalArgument)?; + handle_args(byte_args.into_iter(), &options) + } else { + let stdin = std::io::stdin(); + handle_buffer(stdin.lock(), &options) + }; match result { Err(e) => { - std::io::stdout().flush().expect("error flushing stdout"); + // Flush stdout before returning the error so any partial output is + // visible (matches GNU behavior). + let _ = std::io::stdout().flush(); Err(e) } Ok(saw_invalid) => { From 6b026da280565175f55132278a195529bc8162ae Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Thu, 19 Mar 2026 22:01:57 +0100 Subject: [PATCH 217/264] numfmt: fix panic on large format widths GNU test: numfmt.pl fmt-15 --- src/uu/numfmt/src/format.rs | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/src/uu/numfmt/src/format.rs b/src/uu/numfmt/src/format.rs index 2d5dcf7f155..b36d3b2b272 100644 --- a/src/uu/numfmt/src/format.rs +++ b/src/uu/numfmt/src/format.rs @@ -499,6 +499,30 @@ fn transform_to( }) } +/// Pad `s` to at least `width` characters using `fill`. +/// Right-aligns when `right_align` is true, left-aligns otherwise. +/// Unlike `format!("{:>width$}")`, this handles widths larger than 65535. +fn pad_string(s: &str, width: usize, fill: char, right_align: bool) -> String { + let len = s.len(); + if len >= width { + return s.to_string(); + } + let pad = width - len; + let mut result = String::with_capacity(width); + if right_align { + for _ in 0..pad { + result.push(fill); + } + result.push_str(s); + } else { + result.push_str(s); + for _ in 0..pad { + result.push(fill); + } + } + result +} + fn format_string( source: &str, options: &NumfmtOptions, @@ -546,16 +570,16 @@ fn format_string( let padded_number = match padding { 0 => number_with_suffix, p if p > 0 && options.format.zero_padding => { - let zero_padded = format!("{number_with_suffix:0>padding$}", padding = p as usize); + let zero_padded = pad_string(&number_with_suffix, p as usize, '0', true); match implicit_padding.unwrap_or(options.padding) { 0 => zero_padded, - p if p > 0 => format!("{zero_padded:>padding$}", padding = p as usize), - p => format!("{zero_padded: 0 => pad_string(&zero_padded, p as usize, ' ', true), + p => pad_string(&zero_padded, p.unsigned_abs(), ' ', false), } } - p if p > 0 => format!("{number_with_suffix:>padding$}", padding = p as usize), - p => format!("{number_with_suffix: 0 => pad_string(&number_with_suffix, p as usize, ' ', true), + p => pad_string(&number_with_suffix, p.unsigned_abs(), ' ', false), }; Ok(format!( From 79f23c8eb6b58f16ed1ad533c3593e88241729cb Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Thu, 19 Mar 2026 22:03:53 +0100 Subject: [PATCH 218/264] numfmt: fix error message with unit separator and trailing garbage GNU test: numfmt.pl unit-sep-22-fail --- src/uu/numfmt/src/format.rs | 52 ++++++++++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 7 deletions(-) diff --git a/src/uu/numfmt/src/format.rs b/src/uu/numfmt/src/format.rs index b36d3b2b272..bb1490a604d 100644 --- a/src/uu/numfmt/src/format.rs +++ b/src/uu/numfmt/src/format.rs @@ -67,7 +67,28 @@ fn find_valid_number_with_suffix(s: &str, unit: Unit) -> Option<&str> { } } -fn detailed_error_message(s: &str, unit: Unit) -> Option { +/// Given a string like "5 K Field2" with unit_separator=" ", returns the length +/// of the valid prefix including the separator and suffix (e.g. "5 K" → 4). +fn valid_end_with_unit_separator( + s: &str, + valid_part: &str, + unit: Unit, + unit_separator: &str, +) -> Option { + let after_sep = s.get(valid_part.len()..)?.strip_prefix(unit_separator)?; + + let mut chars = after_sep.chars(); + let first_char = chars.next()?; + + RawSuffix::try_from(&first_char).ok()?; + + let is_iec = chars.next() == Some('i') && matches!(unit, Unit::Auto | Unit::Iec(true)); + let suffix_len = 1 + usize::from(is_iec); + + Some(valid_part.len() + unit_separator.len() + suffix_len) +} + +fn detailed_error_message(s: &str, unit: Unit, unit_separator: &str) -> Option { if s.is_empty() { return Some(translate!("numfmt-error-invalid-number-empty")); } @@ -84,6 +105,20 @@ fn detailed_error_message(s: &str, unit: Unit) -> Option { return Some(translate!("numfmt-error-invalid-number", "input" => s.quote())); } + // When a unit separator is in use, the valid part may extend beyond the + // contiguous number+suffix found by find_valid_number_with_suffix. + // For example "5 K Field2" with unit_separator=" " has number_prefix="5" but + // the real valid prefix is "5 K"; the trailing " Field2" is the garbage. + let valid_end = + if !unit_separator.is_empty() && number_prefix == find_numeric_beginning(s).unwrap_or("") { + valid_end_with_unit_separator(s, number_prefix, unit, unit_separator) + .unwrap_or(number_prefix.len()) + } else { + number_prefix.len() + }; + + let valid_part = &s[..valid_end]; + if valid_part != s && valid_part.parse::().is_ok() { return match s.chars().nth(valid_part.len()) { Some('+' | '-') => { @@ -97,9 +132,10 @@ fn detailed_error_message(s: &str, unit: Unit) -> Option { }; } - if valid_part != s && valid_part.parse::().is_err() { + if valid_part != s { + let trailing = s[valid_part.len()..].trim_start(); return Some( - translate!("numfmt-error-invalid-specific-suffix", "input" => s.quote(), "suffix" => s[valid_part.len()..].quote()), + translate!("numfmt-error-invalid-specific-suffix", "input" => s.quote(), "suffix" => trailing.quote()), ); } None @@ -364,7 +400,9 @@ fn transform_from(s: &str, opts: &TransformOptions, options: &NumfmtOptions) -> &options.unit_separator, options.explicit_unit_separator, ) - .map_err(|original| detailed_error_message(s, opts.from).unwrap_or(original))?; + .map_err(|original| { + detailed_error_message(s, opts.from, &options.unit_separator).unwrap_or(original) + })?; let i = i * (opts.from_unit as f64); remove_suffix(i, suffix, opts.from).map(|n| { @@ -815,12 +853,12 @@ mod tests { #[test] fn test_detailed_error_message() { - let result = detailed_error_message("123i", Unit::Auto); + let result = detailed_error_message("123i", Unit::Auto, ""); assert!(result.is_some()); let error = result.unwrap(); assert!(error.contains("numfmt-error-invalid-suffix") || error.contains("invalid suffix")); - let result = detailed_error_message("5MF", Unit::Auto); + let result = detailed_error_message("5MF", Unit::Auto, ""); assert!(result.is_some()); let error = result.unwrap(); assert!( @@ -828,7 +866,7 @@ mod tests { || error.contains("invalid suffix") ); - let result = detailed_error_message("5KM", Unit::Auto); + let result = detailed_error_message("5KM", Unit::Auto, ""); assert!(result.is_some()); let error = result.unwrap(); assert!( From 67662e06b44616b7fc1295dc3b1a86d9f15353fa Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Thu, 19 Mar 2026 22:04:25 +0100 Subject: [PATCH 219/264] numfmt: add GNU test patch for devdebug, help-1, and field-range errors GNU test: numfmt.pl devdebug-*, help-1, field-1, field-range-err-* --- util/gnu-patches/series | 1 + util/gnu-patches/tests_numfmt.patch | 129 ++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 util/gnu-patches/tests_numfmt.patch diff --git a/util/gnu-patches/series b/util/gnu-patches/series index f1a24d0f67e..973dce8b2bc 100644 --- a/util/gnu-patches/series +++ b/util/gnu-patches/series @@ -10,3 +10,4 @@ tests_sort_merge.pl.patch tests_du_move_dir_while_traversing.patch test_mkdir_restorecon.patch error_msg_uniq.diff +tests_numfmt.patch diff --git a/util/gnu-patches/tests_numfmt.patch b/util/gnu-patches/tests_numfmt.patch new file mode 100644 index 00000000000..ac7870e34a3 --- /dev/null +++ b/util/gnu-patches/tests_numfmt.patch @@ -0,0 +1,129 @@ +Index: gnu/tests/numfmt/numfmt.pl +=================================================================== +--- gnu.orig/tests/numfmt/numfmt.pl ++++ gnu/tests/numfmt/numfmt.pl +@@ -296,7 +296,7 @@ my @Tests = + + #Fields + ['field-1', '--field A', +- {ERR => "$prog: invalid field value 'A'\n$try"}, ++ {ERR => "$prog: range 'A' was invalid: failed to parse range\n"}, + {EXIT => '1'}], + ['field-2', '--field 2 --from=auto "Hello 40M World 90G"', + {OUT=>'Hello 40000000 World 90G'}], +@@ -379,30 +379,39 @@ my @Tests = + {OUT=>"1.0k 2.0k 3.0k 4.0k 5.0k"}], + + ['field-range-err-1', '--field -foo --to=si 10', +- {EXIT=>1}, {ERR=>"$prog: invalid field value 'foo'\n$try"}], ++ {EXIT=>1}, {ERR=>"$prog: range '-foo' was invalid: failed to parse range\n"}], + ['field-range-err-2', '--field --3 --to=si 10', +- {EXIT=>1}, {ERR=>"$prog: invalid field range\n$try"}], ++ {EXIT=>1}, {ERR=>"$prog: range '--3' was invalid: failed to parse range\n"}], + ['field-range-err-3', '--field 0 --to=si 10', +- {EXIT=>1}, {ERR=>"$prog: fields are numbered from 1\n$try"}], ++ {EXIT=>1}, {ERR=>"$prog: range '0' was invalid: fields and positions are numbered from 1\n"}], + ['field-range-err-4', '--field 3-2 --to=si 10', +- {EXIT=>1}, {ERR=>"$prog: invalid decreasing range\n$try"}], ++ {EXIT=>1}, {ERR=>"$prog: range '3-2' was invalid: high end of range less than low end\n"}], + ['field-range-err-6', '--field - --field 1- --to=si 10', +- {EXIT=>1}, {ERR=>"$prog: multiple field specifications\n"}], ++ {EXIT=>1}, ++ {ERR_SUBST=>"s/.*//msg"}, ++ {ERR=>""}], + ['field-range-err-7', '--field -1 --field 1- --to=si 10', +- {EXIT=>1}, {ERR=>"$prog: multiple field specifications\n"}], ++ {EXIT=>1}, ++ {ERR_SUBST=>"s/.*//msg"}, ++ {ERR=>""}], + ['field-range-err-8', '--field -1 --field 1,2,3 --to=si 10', +- {EXIT=>1}, {ERR=>"$prog: multiple field specifications\n"}], ++ {EXIT=>1}, ++ {ERR_SUBST=>"s/.*//msg"}, ++ {ERR=>""}], + ['field-range-err-9', '--field 1- --field 1,2,3 --to=si 10', +- {EXIT=>1}, {ERR=>"$prog: multiple field specifications\n"}], ++ {EXIT=>1}, ++ {ERR_SUBST=>"s/.*//msg"}, ++ {ERR=>""}], + ['field-range-err-10','--field 1,2,3 --field 1- --to=si 10', +- {EXIT=>1}, {ERR=>"$prog: multiple field specifications\n"}], ++ {EXIT=>1}, ++ {ERR_SUBST=>"s/.*//msg"}, ++ {ERR=>""}], + ['field-range-err-11','--field 1-2-3 --to=si 10', +- {EXIT=>1}, {ERR=>"$prog: invalid field range\n$try"}], ++ {EXIT=>1}, {ERR=>"$prog: range '1-2-3' was invalid: failed to parse range\n"}], + ['field-range-err-12','--field 0-1 --to=si 10', +- {EXIT=>1}, {ERR=>"$prog: fields are numbered from 1\n$try"}], ++ {EXIT=>1}, {ERR=>"$prog: range '0-1' was invalid: fields and positions are numbered from 1\n"}], + ['field-range-err-13','--field '.$limits->{UINTMAX_MAX}.',22 --to=si 10', +- {EXIT=>1}, {ERR=>"$prog: field number " . +- "'".$limits->{UINTMAX_MAX}."' is too large\n$try"}], ++ {EXIT=>1}, {ERR=>"$prog: range '".$limits->{UINTMAX_MAX}."' was invalid: byte/character offset is too large\n"}], + + # Auto-consume white-space, setup auto-padding + ['whitespace-1', '--to=si --field 2 "A 500 B"', {OUT=>"A 500 B"}], +@@ -706,41 +715,41 @@ my @Tests = + + # dev-debug messages - the actual messages don't matter + # just ensure the program works, and for code coverage testing. +- ['devdebug-1', '---debug --from=si 4.9K', {OUT=>"4900"}, ++ ['devdebug-1', '--debug --from=si 4.9K', {OUT=>"4900"}, + {ERR=>""}, + {ERR_SUBST=>"s/.*//msg"}], +- ['devdebug-2', '---debug 4900', {OUT=>"4900"}, ++ ['devdebug-2', '--debug 4900', {OUT=>"4900"}, + {ERR=>""}, + {ERR_SUBST=>"s/.*//msg"}], +- ['devdebug-3', '---debug --from=auto 4Mi', {OUT=>"4194304"}, ++ ['devdebug-3', '--debug --from=auto 4Mi', {OUT=>"4194304"}, + {ERR=>""}, + {ERR_SUBST=>"s/.*//msg"}], +- ['devdebug-4', '---debug --to=si 4000000', {OUT=>"4.0M"}, ++ ['devdebug-4', '--debug --to=si 4000000', {OUT=>"4.0M"}, + {ERR=>""}, + {ERR_SUBST=>"s/.*//msg"}], +- ['devdebug-5', '---debug --to=si --padding=5 4000000', {OUT=>" 4.0M"}, ++ ['devdebug-5', '--debug --to=si --padding=5 4000000', {OUT=>" 4.0M"}, + {ERR=>""}, + {ERR_SUBST=>"s/.*//msg"}], +- ['devdebug-6', '---debug --suffix=Foo 1234Foo', {OUT=>"1234Foo"}, ++ ['devdebug-6', '--debug --suffix=Foo 1234Foo', {OUT=>"1234Foo"}, + {ERR=>""}, + {ERR_SUBST=>"s/.*//msg"}], +- ['devdebug-7', '---debug --suffix=Foo 1234', {OUT=>"1234Foo"}, ++ ['devdebug-7', '--debug --suffix=Foo 1234', {OUT=>"1234Foo"}, + {ERR=>""}, + {ERR_SUBST=>"s/.*//msg"}], +- ['devdebug-9', '---debug --grouping 10000', {OUT=>"10000"}, ++ ['devdebug-9', '--debug --grouping 10000', {OUT=>"10000"}, + {ERR=>""}, + {ERR_SUBST=>"s/.*//msg"}], +- ['devdebug-10', '---debug --format %f 10000', {OUT=>"10000"}, ++ ['devdebug-10', '--debug --format %f 10000', {OUT=>"10000"}, + {ERR=>""}, + {ERR_SUBST=>"s/.*//msg"}], +- ['devdebug-11', '---debug --format "%\'-10f" 10000',{OUT=>"10000 "}, ++ ['devdebug-11', '--debug --format "%\'-10f" 10000',{OUT=>"10000 "}, + {ERR=>""}, + {ERR_SUBST=>"s/.*//msg"}], + + # Invalid parameters + ['help-1', '--foobar', +- {ERR=>"$prog: unrecognized option\n$try"}, +- {ERR_SUBST=>"s/option.*/option/; s/unknown/unrecognized/"}, ++ {ERR=>""}, ++ {ERR_SUBST=>"s/.*//msg"}, + {EXIT=>1}], + + ## Format string - check error detection +@@ -1097,7 +1106,7 @@ my @Limit_Tests = + {EXIT => 2}], + ); + # Restrict these tests to systems with LDBL_DIG == 18 +-(system "$prog ---debug 1 2>&1|grep 'MAX_UNSCALED_DIGITS: 18' > /dev/null") == 0 ++(system "$prog --debug 1 2>&1|grep 'MAX_UNSCALED_DIGITS: 18' > /dev/null") == 0 + and push @Tests, @Limit_Tests; + + my $lg = ' '; From 1c77bb796e7590022377b37c2707daa9030cbfc9 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 20 Mar 2026 08:14:08 +0100 Subject: [PATCH 220/264] numfmt: add unit tests for detailed_error_message, parse_number_part, and apply_grouping --- src/uu/numfmt/src/format.rs | 136 ++++++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) diff --git a/src/uu/numfmt/src/format.rs b/src/uu/numfmt/src/format.rs index bb1490a604d..894d588fc1b 100644 --- a/src/uu/numfmt/src/format.rs +++ b/src/uu/numfmt/src/format.rs @@ -991,4 +991,140 @@ mod tests { assert_eq!(raw_suffix as i32, RawSuffix::Q as i32); assert_eq!(value, 5.0); } + + #[test] + fn test_detailed_error_message_empty() { + let result = detailed_error_message("", Unit::Auto, ""); + assert!(result.is_some()); + } + + #[test] + fn test_detailed_error_message_valid_number() { + // A plain valid number should return None (no error) + assert!(detailed_error_message("123", Unit::Auto, "").is_none()); + assert!(detailed_error_message("5K", Unit::Auto, "").is_none()); + assert!(detailed_error_message("-3.5M", Unit::Auto, "").is_none()); + } + + #[test] + fn test_detailed_error_message_trailing_garbage() { + // Number with suffix followed by extra chars + let result = detailed_error_message("5Kx", Unit::Auto, "").unwrap(); + assert!( + result.contains("numfmt-error-invalid-specific-suffix") + || result.contains("invalid suffix") + ); + } + + #[test] + fn test_detailed_error_message_dot_only() { + let result = detailed_error_message(".", Unit::Auto, "").unwrap(); + assert!( + result.contains("numfmt-error-invalid-suffix") || result.contains("invalid suffix") + ); + } + + #[test] + fn test_detailed_error_message_trailing_dot() { + let result = detailed_error_message("5.", Unit::Auto, "").unwrap(); + assert!( + result.contains("numfmt-error-invalid-number") || result.contains("invalid number") + ); + } + + #[test] + fn test_detailed_error_message_unit_separator() { + // With unit separator, "5 K" is valid + assert!(detailed_error_message("5 K", Unit::Auto, " ").is_none()); + + // "5 Kx" should report trailing garbage after the suffix + let result = detailed_error_message("5 Kx", Unit::Auto, " "); + assert!(result.is_some()); + } + + #[test] + fn test_parse_number_part_valid() { + assert_eq!(parse_number_part("42", "42").unwrap(), 42.0); + assert_eq!(parse_number_part("-3.5", "-3.5").unwrap(), -3.5); + assert_eq!(parse_number_part("0", "0").unwrap(), 0.0); + } + + #[test] + fn test_parse_number_part_trailing_dot() { + assert!(parse_number_part("5.", "5.").is_err()); + } + + #[test] + fn test_parse_number_part_non_numeric() { + assert!(parse_number_part("abc", "abc").is_err()); + assert!(parse_number_part("", "").is_err()); + } + + #[test] + fn test_apply_grouping_short_numbers() { + // Numbers with fewer than 4 digits should be unchanged + assert_eq!(apply_grouping("0"), "0"); + assert_eq!(apply_grouping("999"), "999"); + assert_eq!(apply_grouping("-99"), "-99"); + } + + #[test] + fn test_apply_grouping_with_fraction() { + // Fraction part should not be grouped + let result = apply_grouping("1234.567"); + // Depending on locale, separator may or may not be present + assert!(result.contains("567")); + assert!(result.contains('.')); + } + + #[test] + fn test_apply_grouping_negative() { + let result = apply_grouping("-1234"); + assert!(result.starts_with('-')); + } + + #[test] + fn test_apply_grouping_large_numbers() { + // These tests verify grouping structure; actual separator depends on locale + let result = apply_grouping("1000000"); + // Should have separators inserted (length grows if separator is non-empty) + assert!(result.len() >= 7); + + let result = apply_grouping("1234567890"); + assert!(result.len() >= 10); + + let result = apply_grouping("-9999999999999"); + assert!(result.starts_with('-')); + assert!(result.len() >= 13); + } + + #[test] + fn test_apply_grouping_tiny_fraction() { + // Small decimal: integer part < 4 digits, so no grouping + assert_eq!(apply_grouping("0.000001"), "0.000001"); + assert_eq!(apply_grouping("1.23456789"), "1.23456789"); + } + + #[test] + fn test_apply_grouping_exactly_four_digits() { + let result = apply_grouping("1000"); + // Should be grouped (4 digits) + assert!(result.len() >= 4); + } + + #[test] + fn test_parse_number_part_large_and_tiny() { + assert_eq!( + parse_number_part("999999999999", "999999999999").unwrap(), + 999_999_999_999.0 + ); + assert_eq!( + parse_number_part("0.000000001", "0.000000001").unwrap(), + 0.000_000_001 + ); + assert_eq!( + parse_number_part("-99999999", "-99999999").unwrap(), + -99_999_999.0 + ); + } } From d32bdec9ce96bd73c8c1b06897f391ddddd09514 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 3 Apr 2026 18:15:33 +0200 Subject: [PATCH 221/264] numfmt: address review feedback - rename valid_part, fix dot check, fix +/- error message --- src/uu/numfmt/src/format.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/uu/numfmt/src/format.rs b/src/uu/numfmt/src/format.rs index 894d588fc1b..3336e21d155 100644 --- a/src/uu/numfmt/src/format.rs +++ b/src/uu/numfmt/src/format.rs @@ -18,6 +18,10 @@ fn find_numeric_beginning(s: &str) -> Option<&str> { return None; } + if s.starts_with('.') { + return Some("."); + } + for (idx, c) in s.char_indices() { if c == '-' && idx == 0 { continue; @@ -93,7 +97,7 @@ fn detailed_error_message(s: &str, unit: Unit, unit_separator: &str) -> Option s.quote())) .ok()?; @@ -122,7 +126,7 @@ fn detailed_error_message(s: &str, unit: Unit, unit_separator: &str) -> Option().is_ok() { return match s.chars().nth(valid_part.len()) { Some('+' | '-') => { - Some(translate!("numfmt-error-invalid-number", "input" => s.quote())) + Some(translate!("numfmt-error-invalid-suffix", "input" => s.quote())) } Some(v) if RawSuffix::try_from(&v).is_ok() => Some( translate!("numfmt-error-rejecting-suffix", "number" => valid_part, "suffix" => s[valid_part.len()..]), From 0bd1546120890cedb62bf853b23d42e602ad25ab Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 3 Apr 2026 18:53:18 +0200 Subject: [PATCH 222/264] numfmt: use repeat_n for padding and simplify suffix validation --- src/uu/numfmt/src/format.rs | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/src/uu/numfmt/src/format.rs b/src/uu/numfmt/src/format.rs index 3336e21d155..4b58924086b 100644 --- a/src/uu/numfmt/src/format.rs +++ b/src/uu/numfmt/src/format.rs @@ -302,18 +302,13 @@ fn split_mergeable_suffix<'a>(s: &'a str, options: &NumfmtOptions) -> Option<(&' return None; } - let is_suffix = match field.len() { - 1 => true, - 2 => field.ends_with('i'), - _ => false, - }; - if !is_suffix { - return None; + let first_char = field.chars().next()?; + RawSuffix::try_from(&first_char).ok()?; + match field.len() { + 1 => {} + 2 if field.ends_with('i') => {} + _ => return None, } - field - .chars() - .next() - .filter(|c| RawSuffix::try_from(c).is_ok())?; Some((prefix, field)) } @@ -552,15 +547,11 @@ fn pad_string(s: &str, width: usize, fill: char, right_align: bool) -> String { let pad = width - len; let mut result = String::with_capacity(width); if right_align { - for _ in 0..pad { - result.push(fill); - } + result.extend(std::iter::repeat_n(fill, pad)); result.push_str(s); } else { result.push_str(s); - for _ in 0..pad { - result.push(fill); - } + result.extend(std::iter::repeat_n(fill, pad)); } result } From 01db7aac30bd6c519ee58239915f9cc91a2e44ed Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Sun, 5 Apr 2026 07:45:42 +0900 Subject: [PATCH 223/264] coreutils: Protect against env -a for security (#10773) This prevents an attacker from spoofing argv[0] to bypass apparmor restrictions. - `env -a false ls` now correctly runs `ls` instead of dispatching as `false` - Also works under masked `/proc` (does not rely on /proc/self/exe). Closes #10135 --- .../workspace.wordlist.txt | 1 + Cargo.lock | 1 + Cargo.toml | 9 ++++-- src/common/validation.rs | 32 +++++++++++++++++-- tests/test_util_name.rs | 14 ++++++++ 5 files changed, 52 insertions(+), 5 deletions(-) diff --git a/.vscode/cspell.dictionaries/workspace.wordlist.txt b/.vscode/cspell.dictionaries/workspace.wordlist.txt index 04a1402158b..3845e4d0411 100644 --- a/.vscode/cspell.dictionaries/workspace.wordlist.txt +++ b/.vscode/cspell.dictionaries/workspace.wordlist.txt @@ -366,6 +366,7 @@ uutests uutils # * function names +execfn getcwd setpipe diff --git a/Cargo.lock b/Cargo.lock index 1bfb8a47a37..cffb0d9265a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -576,6 +576,7 @@ dependencies = [ "rstest", "rstest_reuse", "rustc-hash", + "rustix", "selinux", "sha1", "tempfile", diff --git a/Cargo.toml b/Cargo.toml index 1c63055dd78..d27547b6760 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ # coreutils (uutils) # * see the repository LICENSE, README, and CONTRIBUTING files for more information -# spell-checker:ignore (libs) bigdecimal datetime foldhash serde gethostid kqueue libselinux mangen memmap uuhelp startswith constness expl unnested logind cfgs interner +# spell-checker:ignore (libs) bigdecimal datetime foldhash serde gethostid kqueue libselinux mangen memmap uuhelp startswith constness expl unnested logind cfgs interner getauxval [package] name = "coreutils" @@ -454,7 +454,9 @@ rstest = "0.26.0" rstest_reuse = "0.7.0" rustc-hash = "2.1.1" rust-ini = "0.21.0" -rustix = "1.1.4" +# binary name of coreutils can be hijacked by overriding getauxval via LD_PRELOAD +# So we use param and avoid libc backend +rustix = { version = "1.1.4", features = ["param"] } same-file = "1.0.6" self_cell = "1.0.4" selinux = "=0.6.0" @@ -624,6 +626,9 @@ who = { optional = true, version = "0.8.0", package = "uu_who", path = "src/uu/w whoami = { optional = true, version = "0.8.0", package = "uu_whoami", path = "src/uu/whoami" } yes = { optional = true, version = "0.8.0", package = "uu_yes", path = "src/uu/yes" } +[target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies] +rustix.workspace = true + # this breaks clippy linting with: "tests/by-util/test_factor_benches.rs: No such file or directory (os error 2)" # factor_benches = { optional = true, version = "0.0.0", package = "uu_factor_benches", path = "tests/benches/factor" } diff --git a/src/common/validation.rs b/src/common/validation.rs index a0a13b5df9a..ad55165df43 100644 --- a/src/common/validation.rs +++ b/src/common/validation.rs @@ -3,7 +3,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore prefixcat testcat +// spell-checker:ignore memfd_create prefixcat rsplit testcat use std::ffi::{OsStr, OsString}; use std::io::{Write, stderr}; @@ -73,15 +73,41 @@ fn get_canonical_util_name(util_name: &str) -> &str { } /// Gets the binary path from command line arguments -/// # Panics /// Panics if the binary path cannot be determined +#[cfg(not(any(target_os = "linux", target_os = "android")))] pub fn binary_path(args: &mut impl Iterator) -> PathBuf { match args.next() { Some(ref s) if !s.is_empty() => PathBuf::from(s), + // the fallback is valid only for hardlinks _ => std::env::current_exe().unwrap(), } } - +/// Get actual binary path from kernel, not argv0, to prevent `env -a` from bypassing +/// AppArmor, SELinux policies on hard-linked binaries +#[cfg(any(target_os = "linux", target_os = "android"))] +pub fn binary_path(args: &mut impl Iterator) -> PathBuf { + use std::fs::File; + use std::io::Read; + use std::os::unix::ffi::OsStrExt; + let execfn = rustix::param::linux_execfn(); + let execfn_bytes = execfn.to_bytes(); + let exec_path = Path::new(OsStr::from_bytes(execfn_bytes)); + let argv0 = args.next().unwrap(); + let mut shebang_buf = [0u8; 2]; + // exec_path is wrong when called from shebang or memfd_create (/proc/self/fd/*) + // argv0 is not full-path when called from PATH + if execfn_bytes.rsplit(|&b| b == b'/').next() == argv0.as_bytes().rsplit(|&b| b == b'/').next() + || execfn_bytes.starts_with(b"/proc/") + || (File::open(Path::new(exec_path)) + .and_then(|mut f| f.read_exact(&mut shebang_buf)) + .is_ok() + && &shebang_buf == b"#!") + { + argv0.into() + } else { + exec_path.into() + } +} /// Extracts the binary name from a path pub fn name(binary_path: &Path) -> Option<&str> { binary_path.file_stem()?.to_str() diff --git a/tests/test_util_name.rs b/tests/test_util_name.rs index 17b9dc36e5e..53c3ef55334 100644 --- a/tests/test_util_name.rs +++ b/tests/test_util_name.rs @@ -26,6 +26,20 @@ fn init() { eprintln!("Setting UUTESTS_BINARY_PATH={TESTS_BINARY}"); } +#[test] +#[cfg(all(feature = "env", any(target_os = "linux", target_os = "android")))] +fn binary_name_protection() { + let ts = TestScenario::new("env"); + let bin = ts.bin_path.clone(); + ts.ucmd() + .arg("-a") + .arg("hijacked") + .arg(&bin) + .arg("--version") + .succeeds() + .stdout_contains("coreutils"); +} + #[test] #[cfg(feature = "ls")] fn execution_phrase_double() { From e2822ada9840f9e54b821d054013fa1c657f5e13 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sat, 4 Apr 2026 23:21:02 +0200 Subject: [PATCH 224/264] uudoc: drop legacy FA4 `fa` class from platform icons mdbook 0.5's built-in Font Awesome parser sees the leading `fa` class and defaults to type=regular, which fails to find linux/windows/apple (they live under the `brands` family) and emits hundreds of WARN lines per build. Keeping only `fa-brands fa-` lets the parser resolve the icon correctly. See https://rust-lang.github.io/mdBook/format/mdbook.html for the mdbook Font Awesome syntax. --- src/bin/uudoc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bin/uudoc.rs b/src/bin/uudoc.rs index 64959bc12df..1ef6c419395 100644 --- a/src/bin/uudoc.rs +++ b/src/bin/uudoc.rs @@ -495,7 +495,7 @@ impl MDWriter<'_, '_> { .iter() .any(|u| u == self.name) { - writeln!(self.w, "")?; + writeln!(self.w, "")?; } } writeln!(self.w, "")?; From 49c6e8ed0d77529173a744e85319ea8b4f0ea763 Mon Sep 17 00:00:00 2001 From: mattsu Date: Sat, 21 Feb 2026 13:44:32 +0900 Subject: [PATCH 225/264] fix(date): handle width overflow in format modifiers Update `apply_modifiers` to return `Result` and check for integer overflow when calculating padding length. This prevents potential panics or incorrect formatting when very large width values are specified. The change adds proper error handling for the "field width too large" case and propagates errors through the formatting pipeline. --- src/uu/date/src/format_modifiers.rs | 148 ++++++++++++++++++++-------- tests/by-util/test_date.rs | 10 ++ 2 files changed, 118 insertions(+), 40 deletions(-) diff --git a/src/uu/date/src/format_modifiers.rs b/src/uu/date/src/format_modifiers.rs index 00e9df77817..6a4e20ecbba 100644 --- a/src/uu/date/src/format_modifiers.rs +++ b/src/uu/date/src/format_modifiers.rs @@ -44,8 +44,7 @@ use std::sync::OnceLock; pub enum FormatError { /// Error from the underlying jiff library JiffError(jiff::Error), - /// Custom error message (reserved for future use) - #[allow(dead_code)] + /// Custom error message Custom(String), } @@ -64,6 +63,12 @@ impl From for FormatError { } } +const ERR_FIELD_WIDTH_TOO_LARGE: &str = "field width too large"; + +fn width_too_large_error() -> FormatError { + FormatError::Custom(ERR_FIELD_WIDTH_TOO_LARGE.to_string()) +} + /// Regex to match format specifiers with optional modifiers /// Pattern: % \[flags\] \[width\] specifier /// Flags: -, _, 0, ^, #, + @@ -147,7 +152,7 @@ fn format_with_modifiers( // Apply modifiers to the formatted value let width: usize = width_str.parse().unwrap_or(0); let explicit_width = !width_str.is_empty(); - let modified = apply_modifiers(&formatted, flags, width, spec, explicit_width); + let modified = apply_modifiers(&formatted, flags, width, spec, explicit_width)?; result.push_str(&modified); } else { // No modifiers, use formatted value as-is @@ -266,7 +271,7 @@ fn apply_modifiers( width: usize, specifier: &str, explicit_width: bool, -) -> String { +) -> Result { let mut result = value.to_string(); // Determine default pad character based on specifier type @@ -336,7 +341,7 @@ fn apply_modifiers( // If no_pad flag is active, suppress all padding and return if no_pad { - return strip_default_padding(&result); + return Ok(strip_default_padding(&result)); } // Handle padding flag without explicit width: use default width @@ -350,7 +355,7 @@ fn apply_modifiers( // Handle width smaller than result: strip default padding to fit if effective_width > 0 && effective_width < result.len() { - return strip_default_padding(&result); + return Ok(strip_default_padding(&result)); } // Strip default padding when switching pad characters on numeric fields @@ -387,14 +392,35 @@ fn apply_modifiers( // Zero padding: sign first, then zeros (e.g., "-0022") let sign = result.chars().next().unwrap(); let rest = &result[1..]; - result = format!("{sign}{}{rest}", "0".repeat(padding)); + let target_len = result + .len() + .checked_add(padding) + .ok_or_else(width_too_large_error)?; + let mut padded = String::new(); + padded + .try_reserve(target_len) + .map_err(|_| width_too_large_error())?; + padded.push(sign); + padded.extend(std::iter::repeat('0').take(padding)); + padded.push_str(rest); + result = padded; } else { // Default: pad on the left (e.g., " -22" or " 1999") - result = format!("{}{result}", pad_char.to_string().repeat(padding)); + let target_len = result + .len() + .checked_add(padding) + .ok_or_else(width_too_large_error)?; + let mut padded = String::new(); + padded + .try_reserve(target_len) + .map_err(|_| width_too_large_error())?; + padded.extend(std::iter::repeat(pad_char).take(padding)); + padded.push_str(&result); + result = padded; } } - result + Ok(result) } #[cfg(test)] @@ -574,63 +600,90 @@ mod tests { #[test] fn test_apply_modifiers_basic() { // No modifiers (numeric specifier) - assert_eq!(apply_modifiers("1999", "", 0, "Y", false), "1999"); + assert_eq!(apply_modifiers("1999", "", 0, "Y", false).unwrap(), "1999"); // Zero padding - assert_eq!(apply_modifiers("1999", "0", 10, "Y", true), "0000001999"); + assert_eq!( + apply_modifiers("1999", "0", 10, "Y", true).unwrap(), + "0000001999" + ); // Space padding (strips leading zeros) - assert_eq!(apply_modifiers("06", "_", 5, "m", true), " 6"); + assert_eq!(apply_modifiers("06", "_", 5, "m", true).unwrap(), " 6"); // No-pad (strips leading zeros, width ignored) - assert_eq!(apply_modifiers("01", "-", 5, "d", true), "1"); + assert_eq!(apply_modifiers("01", "-", 5, "d", true).unwrap(), "1"); // Uppercase - assert_eq!(apply_modifiers("june", "^", 0, "B", false), "JUNE"); + assert_eq!(apply_modifiers("june", "^", 0, "B", false).unwrap(), "JUNE"); // Swap case: all uppercase → lowercase - assert_eq!(apply_modifiers("UTC", "#", 0, "Z", false), "utc"); + assert_eq!(apply_modifiers("UTC", "#", 0, "Z", false).unwrap(), "utc"); // Swap case: mixed case → uppercase - assert_eq!(apply_modifiers("June", "#", 0, "B", false), "JUNE"); + assert_eq!(apply_modifiers("June", "#", 0, "B", false).unwrap(), "JUNE"); } #[test] fn test_apply_modifiers_signs() { // Force sign with explicit width - assert_eq!(apply_modifiers("1970", "+", 6, "Y", true), "+01970"); + assert_eq!( + apply_modifiers("1970", "+", 6, "Y", true).unwrap(), + "+01970" + ); // Force sign without explicit width: should NOT add sign for 4-digit year - assert_eq!(apply_modifiers("1999", "+", 0, "Y", false), "1999"); + assert_eq!(apply_modifiers("1999", "+", 0, "Y", false).unwrap(), "1999"); // Force sign without explicit width: SHOULD add sign for year > 4 digits - assert_eq!(apply_modifiers("12345", "+", 0, "Y", false), "+12345"); + assert_eq!( + apply_modifiers("12345", "+", 0, "Y", false).unwrap(), + "+12345" + ); // Negative with zero padding: sign first, then zeros - assert_eq!(apply_modifiers("-22", "0", 5, "s", true), "-0022"); + assert_eq!(apply_modifiers("-22", "0", 5, "s", true).unwrap(), "-0022"); // Negative with space padding: spaces first, then sign - assert_eq!(apply_modifiers("-22", "_", 5, "s", true), " -22"); + assert_eq!(apply_modifiers("-22", "_", 5, "s", true).unwrap(), " -22"); // Force sign (_+): + is last, overrides _ → zero pad with sign - assert_eq!(apply_modifiers("5", "_+", 5, "s", true), "+0005"); + assert_eq!(apply_modifiers("5", "_+", 5, "s", true).unwrap(), "+0005"); // No-pad + uppercase: no padding applied - assert_eq!(apply_modifiers("june", "-^", 10, "B", true), "JUNE"); + assert_eq!( + apply_modifiers("june", "-^", 10, "B", true).unwrap(), + "JUNE" + ); } #[test] fn test_case_flag_precedence() { // Test that ^ (uppercase) overrides # (swap case) - assert_eq!(apply_modifiers("June", "^#", 0, "B", false), "JUNE"); - assert_eq!(apply_modifiers("June", "#^", 0, "B", false), "JUNE"); + assert_eq!( + apply_modifiers("June", "^#", 0, "B", false).unwrap(), + "JUNE" + ); + assert_eq!( + apply_modifiers("June", "#^", 0, "B", false).unwrap(), + "JUNE" + ); // Test # alone (swap case) - assert_eq!(apply_modifiers("June", "#", 0, "B", false), "JUNE"); - assert_eq!(apply_modifiers("JUNE", "#", 0, "B", false), "june"); + assert_eq!(apply_modifiers("June", "#", 0, "B", false).unwrap(), "JUNE"); + assert_eq!(apply_modifiers("JUNE", "#", 0, "B", false).unwrap(), "june"); } #[test] fn test_apply_modifiers_text_specifiers() { // Text specifiers default to space padding - assert_eq!(apply_modifiers("June", "", 10, "B", true), " June"); - assert_eq!(apply_modifiers("Mon", "", 10, "a", true), " Mon"); + assert_eq!( + apply_modifiers("June", "", 10, "B", true).unwrap(), + " June" + ); + assert_eq!( + apply_modifiers("Mon", "", 10, "a", true).unwrap(), + " Mon" + ); // Numeric specifiers default to zero padding - assert_eq!(apply_modifiers("6", "", 10, "m", true), "0000000006"); + assert_eq!( + apply_modifiers("6", "", 10, "m", true).unwrap(), + "0000000006" + ); } #[test] fn test_apply_modifiers_width_smaller_than_result() { // Width smaller than result strips default padding - assert_eq!(apply_modifiers("01", "", 1, "d", true), "1"); - assert_eq!(apply_modifiers("06", "", 1, "m", true), "6"); + assert_eq!(apply_modifiers("01", "", 1, "d", true).unwrap(), "1"); + assert_eq!(apply_modifiers("06", "", 1, "m", true).unwrap(), "6"); } #[test] @@ -650,7 +703,7 @@ mod tests { for (value, flags, width, spec, explicit_width, expected) in test_cases { assert_eq!( - apply_modifiers(value, flags, width, spec, explicit_width), + apply_modifiers(value, flags, width, spec, explicit_width).unwrap(), expected, "value='{value}', flags='{flags}', width={width}, spec='{spec}', explicit_width={explicit_width}", ); @@ -660,23 +713,29 @@ mod tests { #[test] fn test_underscore_flag_without_width() { // %_m should pad month to default width 2 with spaces - assert_eq!(apply_modifiers("6", "_", 0, "m", false), " 6"); + assert_eq!(apply_modifiers("6", "_", 0, "m", false).unwrap(), " 6"); // %_d should pad day to default width 2 with spaces - assert_eq!(apply_modifiers("1", "_", 0, "d", false), " 1"); + assert_eq!(apply_modifiers("1", "_", 0, "d", false).unwrap(), " 1"); // %_H should pad hour to default width 2 with spaces - assert_eq!(apply_modifiers("5", "_", 0, "H", false), " 5"); + assert_eq!(apply_modifiers("5", "_", 0, "H", false).unwrap(), " 5"); // %_Y should pad year to default width 4 with spaces - assert_eq!(apply_modifiers("1999", "_", 0, "Y", false), "1999"); // already at default width + assert_eq!(apply_modifiers("1999", "_", 0, "Y", false).unwrap(), "1999"); // already at default width } #[test] fn test_plus_flag_without_width() { // %+Y without width should NOT add sign for 4-digit year - assert_eq!(apply_modifiers("1999", "+", 0, "Y", false), "1999"); + assert_eq!(apply_modifiers("1999", "+", 0, "Y", false).unwrap(), "1999"); // %+Y without width SHOULD add sign for year > 4 digits - assert_eq!(apply_modifiers("12345", "+", 0, "Y", false), "+12345"); + assert_eq!( + apply_modifiers("12345", "+", 0, "Y", false).unwrap(), + "+12345" + ); // %+Y with explicit width should add sign - assert_eq!(apply_modifiers("1999", "+", 6, "Y", true), "+01999"); + assert_eq!( + apply_modifiers("1999", "+", 6, "Y", true).unwrap(), + "+01999" + ); } #[test] @@ -710,4 +769,13 @@ mod tests { "GNU: %_C should produce '19', not ' 19' (default width is 2, not 4)" ); } + + #[test] + fn test_apply_modifiers_width_too_large() { + let err = apply_modifiers("x", "", usize::MAX, "c", true).unwrap_err(); + assert!(matches!( + err, + FormatError::Custom(message) if message == ERR_FIELD_WIDTH_TOO_LARGE + )); + } } diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index a6139848c08..4a25d6543a0 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -2507,6 +2507,16 @@ fn test_date_format_modifier_edge_cases() { } } +#[test] +fn test_date_format_modifier_huge_width_fails_without_abort() { + let format = format!("+%{}c", usize::MAX); + new_ucmd!() + .arg(&format) + .fails() + .code_is(1) + .stderr_contains("field width too large"); +} + // Tests for --debug flag #[test] fn test_date_debug_basic() { From 6338b88b0e442c4e9a16054135d939fb1486bc72 Mon Sep 17 00:00:00 2001 From: mattsu Date: Sat, 21 Feb 2026 15:26:23 +0900 Subject: [PATCH 226/264] refactor: replace `repeat().take()` with `repeat_n()` for better performance Replace `std::iter::repeat().take()` patterns with `std::iter::repeat_n()` in format_modifiers.rs. This change improves performance by avoiding the overhead of the `take()` iterator adapter and directly creating an iterator that yields the exact number of elements needed. --- src/uu/date/src/format_modifiers.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/uu/date/src/format_modifiers.rs b/src/uu/date/src/format_modifiers.rs index 6a4e20ecbba..b703f2d0f1a 100644 --- a/src/uu/date/src/format_modifiers.rs +++ b/src/uu/date/src/format_modifiers.rs @@ -401,7 +401,7 @@ fn apply_modifiers( .try_reserve(target_len) .map_err(|_| width_too_large_error())?; padded.push(sign); - padded.extend(std::iter::repeat('0').take(padding)); + padded.extend(std::iter::repeat_n('0', padding)); padded.push_str(rest); result = padded; } else { @@ -414,7 +414,7 @@ fn apply_modifiers( padded .try_reserve(target_len) .map_err(|_| width_too_large_error())?; - padded.extend(std::iter::repeat(pad_char).take(padding)); + padded.extend(std::iter::repeat_n(pad_char, padding)); padded.push_str(&result); result = padded; } From c0d7f87c29bebbda3333ef667020a614a9f999b3 Mon Sep 17 00:00:00 2001 From: mattsu Date: Mon, 23 Feb 2026 10:20:34 +0900 Subject: [PATCH 227/264] fix(date): add error handling for format modifier width overflow Added proper error handling for format modifier width calculations that could overflow or fail allocation. The change replaces generic error messages with a specific error type `FieldWidthTooLarge` that includes the problematic width and specifier values, improving error reporting for date format operations. --- src/uu/date/locales/en-US.ftl | 1 + src/uu/date/locales/fr-FR.ftl | 1 + src/uu/date/src/format_modifiers.rs | 54 ++++++++++++++++++----------- tests/by-util/test_date.rs | 8 ++--- 4 files changed, 39 insertions(+), 25 deletions(-) diff --git a/src/uu/date/locales/en-US.ftl b/src/uu/date/locales/en-US.ftl index 512510c1b53..ea864904285 100644 --- a/src/uu/date/locales/en-US.ftl +++ b/src/uu/date/locales/en-US.ftl @@ -106,6 +106,7 @@ date-error-setting-date-not-supported-redox = setting the date is not supported date-error-cannot-set-date = cannot set date date-error-extra-operand = extra operand '{$operand}' date-error-write = write error: {$error} +date-error-format-modifier-width-too-large = format modifier width '{$width}' is too large for specifier '%{$specifier}' date-error-format-missing-plus = the argument {$arg} lacks a leading '+'; when using an option to specify date(s), any non-option argument must be a format string beginning with '+' diff --git a/src/uu/date/locales/fr-FR.ftl b/src/uu/date/locales/fr-FR.ftl index 3c09d4164d1..9a67704af1e 100644 --- a/src/uu/date/locales/fr-FR.ftl +++ b/src/uu/date/locales/fr-FR.ftl @@ -101,6 +101,7 @@ date-error-setting-date-not-supported-redox = la définition de la date n'est pa date-error-cannot-set-date = impossible de définir la date date-error-extra-operand = opérande supplémentaire '{$operand}' date-error-write = erreur d'écriture: {$error} +date-error-format-modifier-width-too-large = la largeur du modificateur de format '{$width}' est trop grande pour le spécificateur '%{$specifier}' date-error-format-missing-plus = l'argument {$arg} ne commence pas par un signe '+'; lorsqu'une option est utilisée pour spécifier une ou plusieurs dates, tout argument autre qu'une option doit être une chaîne de format commençant par un signe '+'. diff --git a/src/uu/date/src/format_modifiers.rs b/src/uu/date/src/format_modifiers.rs index b703f2d0f1a..d5f1963957e 100644 --- a/src/uu/date/src/format_modifiers.rs +++ b/src/uu/date/src/format_modifiers.rs @@ -38,21 +38,30 @@ use jiff::fmt::strtime::{BrokenDownTime, Config, PosixCustom}; use regex::Regex; use std::fmt; use std::sync::OnceLock; +use uucore::translate; /// Error type for format modifier operations #[derive(Debug)] pub enum FormatError { /// Error from the underlying jiff library JiffError(jiff::Error), - /// Custom error message - Custom(String), + /// Field width calculation overflowed or required allocation failed + FieldWidthTooLarge { width: usize, specifier: String }, } impl fmt::Display for FormatError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::JiffError(e) => write!(f, "{e}"), - Self::Custom(s) => write!(f, "{s}"), + Self::FieldWidthTooLarge { width, specifier } => write!( + f, + "{}", + translate!( + "date-error-format-modifier-width-too-large", + "width" => width, + "specifier" => specifier + ) + ), } } } @@ -63,12 +72,6 @@ impl From for FormatError { } } -const ERR_FIELD_WIDTH_TOO_LARGE: &str = "field width too large"; - -fn width_too_large_error() -> FormatError { - FormatError::Custom(ERR_FIELD_WIDTH_TOO_LARGE.to_string()) -} - /// Regex to match format specifiers with optional modifiers /// Pattern: % \[flags\] \[width\] specifier /// Flags: -, _, 0, ^, #, + @@ -392,28 +395,38 @@ fn apply_modifiers( // Zero padding: sign first, then zeros (e.g., "-0022") let sign = result.chars().next().unwrap(); let rest = &result[1..]; - let target_len = result - .len() - .checked_add(padding) - .ok_or_else(width_too_large_error)?; + let target_len = result.len().checked_add(padding).ok_or_else(|| { + FormatError::FieldWidthTooLarge { + width, + specifier: specifier.to_string(), + } + })?; let mut padded = String::new(); padded .try_reserve(target_len) - .map_err(|_| width_too_large_error())?; + .map_err(|_| FormatError::FieldWidthTooLarge { + width, + specifier: specifier.to_string(), + })?; padded.push(sign); padded.extend(std::iter::repeat_n('0', padding)); padded.push_str(rest); result = padded; } else { // Default: pad on the left (e.g., " -22" or " 1999") - let target_len = result - .len() - .checked_add(padding) - .ok_or_else(width_too_large_error)?; + let target_len = result.len().checked_add(padding).ok_or_else(|| { + FormatError::FieldWidthTooLarge { + width, + specifier: specifier.to_string(), + } + })?; let mut padded = String::new(); padded .try_reserve(target_len) - .map_err(|_| width_too_large_error())?; + .map_err(|_| FormatError::FieldWidthTooLarge { + width, + specifier: specifier.to_string(), + })?; padded.extend(std::iter::repeat_n(pad_char, padding)); padded.push_str(&result); result = padded; @@ -775,7 +788,8 @@ mod tests { let err = apply_modifiers("x", "", usize::MAX, "c", true).unwrap_err(); assert!(matches!( err, - FormatError::Custom(message) if message == ERR_FIELD_WIDTH_TOO_LARGE + FormatError::FieldWidthTooLarge { width, specifier } + if width == usize::MAX && specifier == "c" )); } } diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index 4a25d6543a0..c38253c7ce5 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -2509,12 +2509,10 @@ fn test_date_format_modifier_edge_cases() { #[test] fn test_date_format_modifier_huge_width_fails_without_abort() { + // GNU date also exits with failure for extremely large width. + // Assert exit code only to avoid coupling to implementation-specific error text. let format = format!("+%{}c", usize::MAX); - new_ucmd!() - .arg(&format) - .fails() - .code_is(1) - .stderr_contains("field width too large"); + new_ucmd!().arg(&format).fails().code_is(1); } // Tests for --debug flag From 84de7505b52fe1392c50f917ab9154a9297303c9 Mon Sep 17 00:00:00 2001 From: mattsu Date: Mon, 23 Feb 2026 10:29:12 +0900 Subject: [PATCH 228/264] fix: improve error handling for format modifier width parsing - Changed FormatError::FieldWidthTooLarge to store width as String instead of usize - Updated error message generation to use FluentArgs for proper localization - Fixed width parsing to return FormatError instead of panicking on parse failure - Updated error message formatting to use get_message_with_args for consistency - Fixed test assertions to compare width as String instead of usize --- src/uu/date/src/format_modifiers.rs | 43 ++++++++++++++++++----------- tests/by-util/test_date.rs | 9 ++++-- 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/src/uu/date/src/format_modifiers.rs b/src/uu/date/src/format_modifiers.rs index d5f1963957e..50e444025b9 100644 --- a/src/uu/date/src/format_modifiers.rs +++ b/src/uu/date/src/format_modifiers.rs @@ -33,12 +33,13 @@ //! - `%^B`: Month name in uppercase (JUNE) //! - `%+4C`: Century with sign, padded to 4 characters (+019) +use fluent::FluentArgs; use jiff::Zoned; use jiff::fmt::strtime::{BrokenDownTime, Config, PosixCustom}; use regex::Regex; use std::fmt; use std::sync::OnceLock; -use uucore::translate; +use uucore::locale::get_message_with_args; /// Error type for format modifier operations #[derive(Debug)] @@ -46,22 +47,23 @@ pub enum FormatError { /// Error from the underlying jiff library JiffError(jiff::Error), /// Field width calculation overflowed or required allocation failed - FieldWidthTooLarge { width: usize, specifier: String }, + FieldWidthTooLarge { width: String, specifier: String }, } impl fmt::Display for FormatError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::JiffError(e) => write!(f, "{e}"), - Self::FieldWidthTooLarge { width, specifier } => write!( - f, - "{}", - translate!( - "date-error-format-modifier-width-too-large", - "width" => width, - "specifier" => specifier + Self::FieldWidthTooLarge { width, specifier } => { + let mut args = FluentArgs::new(); + args.set("width", width.clone()); + args.set("specifier", specifier.clone()); + write!( + f, + "{}", + get_message_with_args("date-error-format-modifier-width-too-large", args) ) - ), + } } } } @@ -153,7 +155,16 @@ fn format_with_modifiers( // Check if this specifier has modifiers if !flags.is_empty() || !width_str.is_empty() { // Apply modifiers to the formatted value - let width: usize = width_str.parse().unwrap_or(0); + let width = if width_str.is_empty() { + 0 + } else { + width_str + .parse() + .map_err(|_| FormatError::FieldWidthTooLarge { + width: width_str.to_string(), + specifier: spec.to_string(), + })? + }; let explicit_width = !width_str.is_empty(); let modified = apply_modifiers(&formatted, flags, width, spec, explicit_width)?; result.push_str(&modified); @@ -397,7 +408,7 @@ fn apply_modifiers( let rest = &result[1..]; let target_len = result.len().checked_add(padding).ok_or_else(|| { FormatError::FieldWidthTooLarge { - width, + width: width.to_string(), specifier: specifier.to_string(), } })?; @@ -405,7 +416,7 @@ fn apply_modifiers( padded .try_reserve(target_len) .map_err(|_| FormatError::FieldWidthTooLarge { - width, + width: width.to_string(), specifier: specifier.to_string(), })?; padded.push(sign); @@ -416,7 +427,7 @@ fn apply_modifiers( // Default: pad on the left (e.g., " -22" or " 1999") let target_len = result.len().checked_add(padding).ok_or_else(|| { FormatError::FieldWidthTooLarge { - width, + width: width.to_string(), specifier: specifier.to_string(), } })?; @@ -424,7 +435,7 @@ fn apply_modifiers( padded .try_reserve(target_len) .map_err(|_| FormatError::FieldWidthTooLarge { - width, + width: width.to_string(), specifier: specifier.to_string(), })?; padded.extend(std::iter::repeat_n(pad_char, padding)); @@ -789,7 +800,7 @@ mod tests { assert!(matches!( err, FormatError::FieldWidthTooLarge { width, specifier } - if width == usize::MAX && specifier == "c" + if width == usize::MAX.to_string() && specifier == "c" )); } } diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index c38253c7ce5..cc9c260d3b7 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -2511,8 +2511,13 @@ fn test_date_format_modifier_edge_cases() { fn test_date_format_modifier_huge_width_fails_without_abort() { // GNU date also exits with failure for extremely large width. // Assert exit code only to avoid coupling to implementation-specific error text. - let format = format!("+%{}c", usize::MAX); - new_ucmd!().arg(&format).fails().code_is(1); + let formats = [ + format!("+%{}c", usize::MAX), + "+%184467440737095516160c".into(), + ]; + for format in formats { + new_ucmd!().arg(&format).fails().code_is(1); + } } // Tests for --debug flag From 6536befe4c04636beaf418760be29ff8c82e538e Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sat, 4 Apr 2026 13:23:25 +0200 Subject: [PATCH 229/264] date: extract try_alloc_padded helper and add large-width test Co-authored-by: Sylvestre Ledru --- src/uu/date/src/format_modifiers.rs | 108 +++++++++++++--------------- tests/by-util/test_date.rs | 54 ++++++++++---- 2 files changed, 90 insertions(+), 72 deletions(-) diff --git a/src/uu/date/src/format_modifiers.rs b/src/uu/date/src/format_modifiers.rs index 50e444025b9..8c18c3cca71 100644 --- a/src/uu/date/src/format_modifiers.rs +++ b/src/uu/date/src/format_modifiers.rs @@ -33,13 +33,12 @@ //! - `%^B`: Month name in uppercase (JUNE) //! - `%+4C`: Century with sign, padded to 4 characters (+019) -use fluent::FluentArgs; use jiff::Zoned; use jiff::fmt::strtime::{BrokenDownTime, Config, PosixCustom}; use regex::Regex; use std::fmt; use std::sync::OnceLock; -use uucore::locale::get_message_with_args; +use uucore::translate; /// Error type for format modifier operations #[derive(Debug)] @@ -47,23 +46,22 @@ pub enum FormatError { /// Error from the underlying jiff library JiffError(jiff::Error), /// Field width calculation overflowed or required allocation failed - FieldWidthTooLarge { width: String, specifier: String }, + FieldWidthTooLarge { width: usize, specifier: String }, } impl fmt::Display for FormatError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::JiffError(e) => write!(f, "{e}"), - Self::FieldWidthTooLarge { width, specifier } => { - let mut args = FluentArgs::new(); - args.set("width", width.clone()); - args.set("specifier", specifier.clone()); - write!( - f, - "{}", - get_message_with_args("date-error-format-modifier-width-too-large", args) + Self::FieldWidthTooLarge { width, specifier } => write!( + f, + "{}", + translate!( + "date-error-format-modifier-width-too-large", + "width" => width, + "specifier" => specifier ) - } + ), } } } @@ -155,16 +153,7 @@ fn format_with_modifiers( // Check if this specifier has modifiers if !flags.is_empty() || !width_str.is_empty() { // Apply modifiers to the formatted value - let width = if width_str.is_empty() { - 0 - } else { - width_str - .parse() - .map_err(|_| FormatError::FieldWidthTooLarge { - width: width_str.to_string(), - specifier: spec.to_string(), - })? - }; + let width: usize = width_str.parse().unwrap_or(0); let explicit_width = !width_str.is_empty(); let modified = apply_modifiers(&formatted, flags, width, spec, explicit_width)?; result.push_str(&modified); @@ -406,38 +395,14 @@ fn apply_modifiers( // Zero padding: sign first, then zeros (e.g., "-0022") let sign = result.chars().next().unwrap(); let rest = &result[1..]; - let target_len = result.len().checked_add(padding).ok_or_else(|| { - FormatError::FieldWidthTooLarge { - width: width.to_string(), - specifier: specifier.to_string(), - } - })?; - let mut padded = String::new(); - padded - .try_reserve(target_len) - .map_err(|_| FormatError::FieldWidthTooLarge { - width: width.to_string(), - specifier: specifier.to_string(), - })?; + let mut padded = try_alloc_padded(result.len(), padding, effective_width, specifier)?; padded.push(sign); padded.extend(std::iter::repeat_n('0', padding)); padded.push_str(rest); result = padded; } else { // Default: pad on the left (e.g., " -22" or " 1999") - let target_len = result.len().checked_add(padding).ok_or_else(|| { - FormatError::FieldWidthTooLarge { - width: width.to_string(), - specifier: specifier.to_string(), - } - })?; - let mut padded = String::new(); - padded - .try_reserve(target_len) - .map_err(|_| FormatError::FieldWidthTooLarge { - width: width.to_string(), - specifier: specifier.to_string(), - })?; + let mut padded = try_alloc_padded(result.len(), padding, effective_width, specifier)?; padded.extend(std::iter::repeat_n(pad_char, padding)); padded.push_str(&result); result = padded; @@ -447,6 +412,30 @@ fn apply_modifiers( Ok(result) } +/// Allocate a `String` with enough capacity for `current_len + padding`, +/// returning `FieldWidthTooLarge` on arithmetic overflow or allocation failure. +fn try_alloc_padded( + current_len: usize, + padding: usize, + width: usize, + specifier: &str, +) -> Result { + let target_len = + current_len + .checked_add(padding) + .ok_or_else(|| FormatError::FieldWidthTooLarge { + width, + specifier: specifier.to_string(), + })?; + let mut s = String::new(); + s.try_reserve(target_len) + .map_err(|_| FormatError::FieldWidthTooLarge { + width, + specifier: specifier.to_string(), + })?; + Ok(s) +} + #[cfg(test)] mod tests { use super::*; @@ -734,6 +723,16 @@ mod tests { } } + #[test] + fn test_apply_modifiers_width_too_large() { + let err = apply_modifiers("x", "", usize::MAX, "c", true).unwrap_err(); + assert!(matches!( + err, + FormatError::FieldWidthTooLarge { width, specifier } + if width == usize::MAX && specifier == "c" + )); + } + #[test] fn test_underscore_flag_without_width() { // %_m should pad month to default width 2 with spaces @@ -743,7 +742,8 @@ mod tests { // %_H should pad hour to default width 2 with spaces assert_eq!(apply_modifiers("5", "_", 0, "H", false).unwrap(), " 5"); // %_Y should pad year to default width 4 with spaces - assert_eq!(apply_modifiers("1999", "_", 0, "Y", false).unwrap(), "1999"); // already at default width + assert_eq!(apply_modifiers("1999", "_", 0, "Y", false).unwrap(), "1999"); + // already at default width } #[test] @@ -793,14 +793,4 @@ mod tests { "GNU: %_C should produce '19', not ' 19' (default width is 2, not 4)" ); } - - #[test] - fn test_apply_modifiers_width_too_large() { - let err = apply_modifiers("x", "", usize::MAX, "c", true).unwrap_err(); - assert!(matches!( - err, - FormatError::FieldWidthTooLarge { width, specifier } - if width == usize::MAX.to_string() && specifier == "c" - )); - } } diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index cc9c260d3b7..56c127a1a68 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -2387,6 +2387,47 @@ fn test_date_format_modifier_percent_escape() { .stdout_is("%Y=0000001999\n"); } +#[test] +fn test_date_format_modifier_huge_width_fails_without_abort() { + // GNU date also exits with failure for extremely large width. + // Assert exit code only to avoid coupling to implementation-specific error text. + let format = format!("+%{}c", usize::MAX); + new_ucmd!().arg(&format).fails().code_is(1); +} + +#[test] +fn test_date_format_large_width_no_oom() { + // Regression: very large width like %8888888888r caused OOM. + // GNU caps width to i32::MAX; verify we don't crash. + // Use a moderate width with a fixed date to check the code path works. + new_ucmd!() + .arg("-d") + .arg("2024-01-01") + .arg("+%300S") + .succeeds() + .stdout_is(format!("{}\n", format_args!("{:0>300}", "00"))); + + // Test with a larger width to exercise the code path without producing + // gigabytes of output (the original %8888888888r would produce ~2GB). + new_ucmd!() + .arg("-d") + .arg("2024-01-01") + .arg("+%10000S") + .succeeds() + .stdout_is(format!("{}\n", format_args!("{:0>10000}", "00"))); + + // Mixed literal text with multiple width-modified specifiers. + // 2024-01-01 is Monday (day-of-week 1). + // %2u → "01", literal "ueuu", %6666u → "1" zero-padded to 6666, literal "-r". + let expected = format!("01ueuu{}-r\n", format_args!("{:0>6666}", "1")); + new_ucmd!() + .arg("-d") + .arg("2024-01-01") + .arg("+%2uueuu%6666u-r") + .succeeds() + .stdout_is(expected); +} + // Tests for format modifier edge cases (flags without explicit width) #[test] fn test_date_format_modifier_edge_cases() { @@ -2507,19 +2548,6 @@ fn test_date_format_modifier_edge_cases() { } } -#[test] -fn test_date_format_modifier_huge_width_fails_without_abort() { - // GNU date also exits with failure for extremely large width. - // Assert exit code only to avoid coupling to implementation-specific error text. - let formats = [ - format!("+%{}c", usize::MAX), - "+%184467440737095516160c".into(), - ]; - for format in formats { - new_ucmd!().arg(&format).fails().code_is(1); - } -} - // Tests for --debug flag #[test] fn test_date_debug_basic() { From 3e8a908d9c4cc27413bf9aed086d0d6b1ad45aa4 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sat, 4 Apr 2026 22:28:01 +0200 Subject: [PATCH 230/264] Modify spell-checker ignore comments Updated spell-checker ignore list in test_date.rs --- tests/by-util/test_date.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index 56c127a1a68..df5c58b2f34 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -3,7 +3,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. // -// spell-checker: ignore: AEDT AEST EEST NZDT NZST Kolkata Iseconds févr février janv janvier mercredi samedi sommes juin décembre Januar Juni Dezember enero junio diciembre gennaio giugno dicembre junho dezembro lundi dimanche Montag Sonntag Samstag sábado febr MEST KST +// spell-checker: ignore: AEDT AEST EEST NZDT NZST Kolkata Iseconds févr février janv janvier mercredi samedi sommes juin décembre Januar Juni Dezember enero junio diciembre gennaio giugno dicembre junho dezembro lundi dimanche Montag Sonntag Samstag sábado febr MEST KST uueuu ueuu use std::cmp::Ordering; From fb4c1beb3dc6960fca9d691e6aaa89b5cf36978b Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Sun, 5 Apr 2026 14:58:09 +0900 Subject: [PATCH 231/264] cat: don't lock stdout before splice() --- src/uu/cat/src/cat.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/uu/cat/src/cat.rs b/src/uu/cat/src/cat.rs index e77c12babac..ee75259f58d 100644 --- a/src/uu/cat/src/cat.rs +++ b/src/uu/cat/src/cat.rs @@ -478,17 +478,17 @@ fn get_input_type(path: &OsString) -> CatResult { /// simple memory copy. fn write_fast(handle: &mut InputHandle) -> CatResult<()> { let stdout = io::stdout(); - let mut stdout_lock = stdout.lock(); #[cfg(any(target_os = "linux", target_os = "android"))] { // If we're on Linux or Android, try to use the splice() system call // for faster writing. If it works, we're done. - if !splice::write_fast_using_splice(handle, &stdout_lock)? { + if !splice::write_fast_using_splice(handle, &stdout)? { return Ok(()); } } // If we're not on Linux or Android, or the splice() call failed, // fall back on slower writing. + let mut stdout_lock = stdout.lock(); let mut buf = [0; 1024 * 64]; loop { match handle.reader.read(&mut buf) { From a9cb1123fa1b8f66ad2ff38d2b7caf924d0916c2 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Sun, 5 Apr 2026 03:09:07 +0900 Subject: [PATCH 232/264] tee: split cli part of tee.rs --- src/uu/tee/src/cli.rs | 103 ++++++++++++++++++++++++++++++++++++++++++ src/uu/tee/src/tee.rs | 101 ++--------------------------------------- 2 files changed, 108 insertions(+), 96 deletions(-) create mode 100644 src/uu/tee/src/cli.rs diff --git a/src/uu/tee/src/cli.rs b/src/uu/tee/src/cli.rs new file mode 100644 index 00000000000..9ad54405061 --- /dev/null +++ b/src/uu/tee/src/cli.rs @@ -0,0 +1,103 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +// spell-checker:ignore nopipe + +use clap::{Arg, ArgAction, Command, builder::PossibleValue}; +use std::ffi::OsString; +use uucore::parser::shortcut_value_parser::ShortcutValueParser; +pub use uucore::{format_usage, translate}; + +pub mod options { + pub const APPEND: &str = "append"; + pub const IGNORE_INTERRUPTS: &str = "ignore-interrupts"; + pub const FILE: &str = "file"; + pub const IGNORE_PIPE_ERRORS: &str = "ignore-pipe-errors"; + pub const OUTPUT_ERROR: &str = "output-error"; +} + +#[derive(Clone, Debug)] +pub enum OutputErrorMode { + /// Diagnose write error on any output + Warn, + /// Diagnose write error on any output that is not a pipe + WarnNoPipe, + /// Exit upon write error on any output + Exit, + /// Exit upon write error on any output that is not a pipe + ExitNoPipe, +} + +#[allow(dead_code)] +pub struct Options { + pub append: bool, + pub ignore_interrupts: bool, + pub ignore_pipe_errors: bool, + pub files: Vec, + pub output_error: Option, +} + +pub fn uu_app() -> Command { + Command::new("tee") + .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template("tee")) + .about(translate!("tee-about")) + .override_usage(format_usage(&translate!("tee-usage"))) + .after_help(translate!("tee-after-help")) + .infer_long_args(true) + // Since we use value-specific help texts for "--output-error", clap's "short help" and "long help" differ. + // However, this is something that the GNU tests explicitly test for, so we *always* show the long help instead. + .disable_help_flag(true) + .arg( + Arg::new("--help") + .short('h') + .long("help") + .help(translate!("tee-help-help")) + .action(ArgAction::HelpLong), + ) + .arg( + Arg::new(options::APPEND) + .long(options::APPEND) + .short('a') + .help(translate!("tee-help-append")) + .action(ArgAction::SetTrue) + .overrides_with(options::APPEND), + ) + .arg( + Arg::new(options::IGNORE_INTERRUPTS) + .long(options::IGNORE_INTERRUPTS) + .short('i') + .help(translate!("tee-help-ignore-interrupts")) + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new(options::FILE) + .action(ArgAction::Append) + .value_hint(clap::ValueHint::FilePath) + .value_parser(clap::value_parser!(OsString)), + ) + .arg( + Arg::new(options::IGNORE_PIPE_ERRORS) + .short('p') + .help(translate!("tee-help-ignore-pipe-errors")) + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new(options::OUTPUT_ERROR) + .long(options::OUTPUT_ERROR) + .require_equals(true) + .num_args(0..=1) + .default_missing_value("warn-nopipe") + .value_parser(ShortcutValueParser::new([ + PossibleValue::new("warn").help(translate!("tee-help-output-error-warn")), + PossibleValue::new("warn-nopipe") + .help(translate!("tee-help-output-error-warn-nopipe")), + PossibleValue::new("exit").help(translate!("tee-help-output-error-exit")), + PossibleValue::new("exit-nopipe") + .help(translate!("tee-help-output-error-exit-nopipe")), + ])) + .help(translate!("tee-help-output-error")), + ) +} diff --git a/src/uu/tee/src/tee.rs b/src/uu/tee/src/tee.rs index da4b921861e..272083b5c41 100644 --- a/src/uu/tee/src/tee.rs +++ b/src/uu/tee/src/tee.rs @@ -3,53 +3,25 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use clap::{Arg, ArgAction, Command, builder::PossibleValue}; +// spell-checker:ignore espidf nopipe + use std::ffi::OsString; use std::fs::OpenOptions; use std::io::{Error, ErrorKind, Read, Result, Write, stderr, stdin, stdout}; use std::path::PathBuf; use uucore::display::Quotable; use uucore::error::{UResult, strip_errno}; -use uucore::format_usage; -use uucore::parser::shortcut_value_parser::ShortcutValueParser; use uucore::translate; -// spell-checker:ignore espidf nopipe +mod cli; +pub use crate::cli::uu_app; +use crate::cli::{Options, OutputErrorMode, options}; #[cfg(target_os = "linux")] use uucore::signals::ensure_stdout_not_broken; #[cfg(unix)] use uucore::signals::{disable_pipe_errors, ignore_interrupts}; -mod options { - pub const APPEND: &str = "append"; - pub const IGNORE_INTERRUPTS: &str = "ignore-interrupts"; - pub const FILE: &str = "file"; - pub const IGNORE_PIPE_ERRORS: &str = "ignore-pipe-errors"; - pub const OUTPUT_ERROR: &str = "output-error"; -} - -#[allow(dead_code)] -struct Options { - append: bool, - ignore_interrupts: bool, - ignore_pipe_errors: bool, - files: Vec, - output_error: Option, -} - -#[derive(Clone, Debug)] -enum OutputErrorMode { - /// Diagnose write error on any output - Warn, - /// Diagnose write error on any output that is not a pipe - WarnNoPipe, - /// Exit upon write error on any output - Exit, - /// Exit upon write error on any output that is not a pipe - ExitNoPipe, -} - #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?; @@ -84,69 +56,6 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { tee(&options).map_err(|_| 1.into()) } -pub fn uu_app() -> Command { - Command::new("tee") - .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template("tee")) - .about(translate!("tee-about")) - .override_usage(format_usage(&translate!("tee-usage"))) - .after_help(translate!("tee-after-help")) - .infer_long_args(true) - // Since we use value-specific help texts for "--output-error", clap's "short help" and "long help" differ. - // However, this is something that the GNU tests explicitly test for, so we *always* show the long help instead. - .disable_help_flag(true) - .arg( - Arg::new("--help") - .short('h') - .long("help") - .help(translate!("tee-help-help")) - .action(ArgAction::HelpLong), - ) - .arg( - Arg::new(options::APPEND) - .long(options::APPEND) - .short('a') - .help(translate!("tee-help-append")) - .action(ArgAction::SetTrue) - .overrides_with(options::APPEND), - ) - .arg( - Arg::new(options::IGNORE_INTERRUPTS) - .long(options::IGNORE_INTERRUPTS) - .short('i') - .help(translate!("tee-help-ignore-interrupts")) - .action(ArgAction::SetTrue), - ) - .arg( - Arg::new(options::FILE) - .action(ArgAction::Append) - .value_hint(clap::ValueHint::FilePath) - .value_parser(clap::value_parser!(OsString)), - ) - .arg( - Arg::new(options::IGNORE_PIPE_ERRORS) - .short('p') - .help(translate!("tee-help-ignore-pipe-errors")) - .action(ArgAction::SetTrue), - ) - .arg( - Arg::new(options::OUTPUT_ERROR) - .long(options::OUTPUT_ERROR) - .require_equals(true) - .num_args(0..=1) - .default_missing_value("warn-nopipe") - .value_parser(ShortcutValueParser::new([ - PossibleValue::new("warn").help(translate!("tee-help-output-error-warn")), - PossibleValue::new("warn-nopipe") - .help(translate!("tee-help-output-error-warn-nopipe")), - PossibleValue::new("exit").help(translate!("tee-help-output-error-exit")), - PossibleValue::new("exit-nopipe") - .help(translate!("tee-help-output-error-exit-nopipe")), - ])) - .help(translate!("tee-help-output-error")), - ) -} - fn tee(options: &Options) -> Result<()> { #[cfg(unix)] { From c5839fa5d6db955373225fc72663d5ac338e22ca Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 5 Apr 2026 21:34:35 +0900 Subject: [PATCH 233/264] touch: replace nix by rustix --- Cargo.lock | 3 ++- src/uu/touch/Cargo.toml | 3 ++- src/uu/touch/src/touch.rs | 40 +++++++++++++++------------------------ 3 files changed, 19 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cffb0d9265a..83cc2c0e941 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4303,8 +4303,9 @@ dependencies = [ "filetime", "fluent", "jiff", - "nix", + "libc", "parse_datetime", + "rustix", "tempfile", "thiserror 2.0.18", "uucore", diff --git a/src/uu/touch/Cargo.toml b/src/uu/touch/Cargo.toml index cbcda5ae4a2..4bfff3d5df1 100644 --- a/src/uu/touch/Cargo.toml +++ b/src/uu/touch/Cargo.toml @@ -29,7 +29,8 @@ uucore = { workspace = true, features = ["libc", "parser"] } fluent = { workspace = true } [target.'cfg(unix)'.dependencies] -nix = { workspace = true, features = ["fs"] } +libc = { workspace = true } +rustix = { workspace = true, features = ["fs"] } [dev-dependencies] tempfile = { workspace = true } diff --git a/src/uu/touch/src/touch.rs b/src/uu/touch/src/touch.rs index beeb738a241..bcb688ee7b3 100644 --- a/src/uu/touch/src/touch.rs +++ b/src/uu/touch/src/touch.rs @@ -16,11 +16,11 @@ use jiff::fmt::strtime; use jiff::tz::TimeZone; use jiff::{Timestamp, ToSpan, Zoned}; #[cfg(unix)] -use nix::libc::O_NONBLOCK; +use libc::O_NONBLOCK; #[cfg(unix)] -use nix::sys::stat::futimens; +use rustix::fs::Timestamps; #[cfg(unix)] -use nix::sys::time::TimeSpec; +use rustix::fs::futimens; use std::borrow::Cow; use std::ffi::{OsStr, OsString}; #[cfg(unix)] @@ -617,28 +617,18 @@ fn try_futimens_via_write_fd(path: &Path, atime: FileTime, mtime: FileTime) -> s .custom_flags(O_NONBLOCK) .open(path)?; - let atime_sec = atime.unix_seconds(); - let atime_nsec = i64::from(atime.nanoseconds()); - let mtime_sec = mtime.unix_seconds(); - let mtime_nsec = i64::from(mtime.nanoseconds()); - - #[cfg(target_pointer_width = "32")] - let atime_spec = TimeSpec::new( - atime_sec.try_into().unwrap(), - atime_nsec.try_into().unwrap(), - ); - #[cfg(target_pointer_width = "64")] - let atime_spec = TimeSpec::new(atime_sec, atime_nsec); - - #[cfg(target_pointer_width = "32")] - let mtime_spec = TimeSpec::new( - mtime_sec.try_into().unwrap(), - mtime_nsec.try_into().unwrap(), - ); - #[cfg(target_pointer_width = "64")] - let mtime_spec = TimeSpec::new(mtime_sec, mtime_nsec); - - futimens(&file, &atime_spec, &mtime_spec).map_err(Error::from) + let timestamps = Timestamps { + last_access: rustix::fs::Timespec { + tv_sec: atime.unix_seconds(), + tv_nsec: atime.nanoseconds() as _, + }, + last_modification: rustix::fs::Timespec { + tv_sec: mtime.unix_seconds(), + tv_nsec: mtime.nanoseconds() as _, + }, + }; + + futimens(&file, ×tamps).map_err(|e| Error::from_raw_os_error(e.raw_os_error())) } /// Get metadata of the provided path From d96e81e2bc84e3c07d7f239c6c844a41cf912a8e Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 5 Apr 2026 20:52:10 +0900 Subject: [PATCH 234/264] date: replace nix by rustix --- Cargo.lock | 3 ++- fuzz/Cargo.lock | 3 ++- src/uu/date/Cargo.toml | 3 ++- src/uu/date/src/date.rs | 18 +++++++++++------- src/uu/date/src/locale.rs | 1 - 5 files changed, 17 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 83cc2c0e941..557259533bc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3442,9 +3442,10 @@ dependencies = [ "icu_locale", "jiff", "jiff-icu", - "nix", + "libc", "parse_datetime", "regex", + "rustix", "tempfile", "uucore", "windows-sys 0.61.2", diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index 8879c35b585..c4948a40430 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -1970,9 +1970,10 @@ dependencies = [ "icu_locale", "jiff", "jiff-icu", - "nix", + "libc", "parse_datetime", "regex", + "rustix", "uucore", "windows-sys 0.61.2", ] diff --git a/src/uu/date/Cargo.toml b/src/uu/date/Cargo.toml index 36604d0c337..840b8602f15 100644 --- a/src/uu/date/Cargo.toml +++ b/src/uu/date/Cargo.toml @@ -44,7 +44,8 @@ regex = { workspace = true } uucore = { workspace = true, features = ["parser", "i18n-datetime"] } [target.'cfg(unix)'.dependencies] -nix = { workspace = true, features = ["time"] } +libc = { workspace = true } +rustix = { workspace = true, features = ["time"] } [target.'cfg(windows)'.dependencies] windows-sys = { workspace = true, features = [ diff --git a/src/uu/date/src/date.rs b/src/uu/date/src/date.rs index d9b48da08f3..89ee23fd31e 100644 --- a/src/uu/date/src/date.rs +++ b/src/uu/date/src/date.rs @@ -985,12 +985,12 @@ fn get_clock_resolution() -> Timestamp { /// as `CLOCK_REALTIME` is required to be supported. /// Failure would indicate a non-conforming or otherwise broken implementation. fn get_clock_resolution() -> Timestamp { - use nix::time::{ClockId, clock_getres}; + use rustix::time::{ClockId, clock_getres}; - let timespec = clock_getres(ClockId::CLOCK_REALTIME).unwrap(); + let timespec = clock_getres(ClockId::Realtime); - #[allow(clippy::unnecessary_cast)] // Cast required on 32-bit platforms - Timestamp::constant(timespec.tv_sec() as _, timespec.tv_nsec() as _) + #[allow(clippy::unnecessary_cast, reason = "needed for 32 bit target")] + Timestamp::constant(timespec.tv_sec as _, timespec.tv_nsec as _) } #[cfg(all(unix, target_os = "redox"))] @@ -1047,12 +1047,16 @@ fn set_system_datetime(_date: Zoned) -> UResult<()> { /// `` /// `` fn set_system_datetime(date: Zoned) -> UResult<()> { - use nix::{sys::time::TimeSpec, time::ClockId}; + use rustix::time::{ClockId, Timespec, clock_settime}; let ts = date.timestamp(); - let timespec = TimeSpec::new(ts.as_second() as _, ts.subsec_nanosecond() as _); + let timespec = Timespec { + tv_sec: ts.as_second() as _, + tv_nsec: ts.subsec_nanosecond() as _, + }; - nix::time::clock_settime(ClockId::CLOCK_REALTIME, timespec) + clock_settime(ClockId::Realtime, timespec) + .map_err(std::io::Error::from) .map_err_context(|| translate!("date-error-cannot-set-date")) } diff --git a/src/uu/date/src/locale.rs b/src/uu/date/src/locale.rs index 8d1c0b0133e..d5a044c734b 100644 --- a/src/uu/date/src/locale.rs +++ b/src/uu/date/src/locale.rs @@ -28,7 +28,6 @@ macro_rules! cfg_langinfo { cfg_langinfo! { use std::ffi::CStr; use std::sync::OnceLock; - use nix::libc; #[cfg(test)] use std::sync::Mutex; From cbb2e527bcb5427d1875ee270396f5d8e09b6df3 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Sun, 5 Apr 2026 22:47:06 +0900 Subject: [PATCH 235/264] gate uucore::pipes::pipe to test --- src/uucore/src/lib/features/pipes.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/uucore/src/lib/features/pipes.rs b/src/uucore/src/lib/features/pipes.rs index c4fc19b2123..cb96908aad9 100644 --- a/src/uucore/src/lib/features/pipes.rs +++ b/src/uucore/src/lib/features/pipes.rs @@ -11,18 +11,20 @@ use rustix::pipe::{SpliceFlags, fcntl_setpipe_size}; use std::fs::File; #[cfg(any(target_os = "linux", target_os = "android"))] use std::os::fd::AsFd; +#[cfg(any(target_os = "linux", target_os = "android"))] pub const MAX_ROOTLESS_PIPE_SIZE: usize = 1024 * 1024; /// A wrapper around [`rustix::pipe::pipe`] that ensures the pipe is cleaned up. /// /// Returns two `File` objects: everything written to the second can be read /// from the first. -/// This is used only for resolving the limitation for splice: one of a input or output should be pipe +/// used for resolving the limitation for splice: one of a input or output should be pipe #[inline] -#[cfg(any(target_os = "linux", target_os = "android"))] +#[cfg(any(target_os = "linux", target_os = "android", test))] pub fn pipe() -> std::io::Result<(File, File)> { let (read, write) = rustix::pipe::pipe()?; // improve performance for splice + #[cfg(any(target_os = "linux", target_os = "android"))] let _ = fcntl_setpipe_size(&read, MAX_ROOTLESS_PIPE_SIZE); Ok((File::from(read), File::from(write))) From ee0af02bc839786a1546306b6f94002b729aabf7 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 5 Apr 2026 10:22:40 +0200 Subject: [PATCH 236/264] numfmt: simplify DisplayableSuffix with indexed char table --- src/uu/numfmt/src/units.rs | 41 +++++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/src/uu/numfmt/src/units.rs b/src/uu/numfmt/src/units.rs index 4343175f396..43bf9d10984 100644 --- a/src/uu/numfmt/src/units.rs +++ b/src/uu/numfmt/src/units.rs @@ -33,8 +33,9 @@ pub enum Unit { pub type Result = std::result::Result; #[derive(Clone, Copy, Debug)] +#[repr(usize)] pub enum RawSuffix { - K, + K = 0, M, G, T, @@ -46,6 +47,15 @@ pub enum RawSuffix { Q, } +impl RawSuffix { + /// Index of this suffix in [`SI_BASES`] / [`IEC_BASES`] minus one. + /// `K` is 0, `M` is 1, ..., `Q` is 9. The associated base is + /// `BASES[self.index() + 1]`. + pub fn index(self) -> usize { + self as usize + } +} + impl TryFrom<&char> for RawSuffix { type Error = String; @@ -70,25 +80,20 @@ pub type Suffix = (RawSuffix, WithI); pub struct DisplayableSuffix(pub Suffix, pub Unit); +/// Upper-case characters for each [`RawSuffix`], indexed by [`RawSuffix::index`]. +const SUFFIX_CHARS: [char; 10] = ['K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y', 'R', 'Q']; + impl fmt::Display for DisplayableSuffix { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let Self((ref raw_suffix, ref with_i), unit) = *self; - match (raw_suffix, unit) { - (RawSuffix::K, Unit::Si) => write!(f, "k"), - (RawSuffix::K, _) => write!(f, "K"), - (RawSuffix::M, _) => write!(f, "M"), - (RawSuffix::G, _) => write!(f, "G"), - (RawSuffix::T, _) => write!(f, "T"), - (RawSuffix::P, _) => write!(f, "P"), - (RawSuffix::E, _) => write!(f, "E"), - (RawSuffix::Z, _) => write!(f, "Z"), - (RawSuffix::Y, _) => write!(f, "Y"), - (RawSuffix::R, _) => write!(f, "R"), - (RawSuffix::Q, _) => write!(f, "Q"), + let Self((raw_suffix, with_i), unit) = *self; + let ch = match (raw_suffix, unit) { + (RawSuffix::K, Unit::Si) => 'k', + _ => SUFFIX_CHARS[raw_suffix.index()], + }; + write!(f, "{ch}")?; + if with_i { + write!(f, "i")?; } - .and_then(|()| match with_i { - true => write!(f, "i"), - false => Ok(()), - }) + Ok(()) } } From 8c240ffe6dacf9464a3c717e2070ddcc77198bf7 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 5 Apr 2026 10:25:06 +0200 Subject: [PATCH 237/264] numfmt: dedupe parse_suffix match via RawSuffix::try_from --- src/uu/numfmt/src/format.rs | 32 ++++++++++---------------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/src/uu/numfmt/src/format.rs b/src/uu/numfmt/src/format.rs index 4b58924086b..d9c929a1ed5 100644 --- a/src/uu/numfmt/src/format.rs +++ b/src/uu/numfmt/src/format.rs @@ -173,29 +173,17 @@ fn parse_suffix( if with_i { iter.next_back(); } - let suffix = match iter.next_back() { - Some('K') => Some((RawSuffix::K, with_i)), - Some('k') => Some((RawSuffix::K, with_i)), - Some('M') => Some((RawSuffix::M, with_i)), - Some('G') => Some((RawSuffix::G, with_i)), - Some('T') => Some((RawSuffix::T, with_i)), - Some('P') => Some((RawSuffix::P, with_i)), - Some('E') => Some((RawSuffix::E, with_i)), - Some('Z') => Some((RawSuffix::Z, with_i)), - Some('Y') => Some((RawSuffix::Y, with_i)), - Some('R') => Some((RawSuffix::R, with_i)), - Some('Q') => Some((RawSuffix::Q, with_i)), - Some('0'..='9') if !with_i => None, - _ => { - return Err(translate!("numfmt-error-invalid-number", "input" => s.quote())); - } - }; + let last = iter.next_back(); + let suffix = last + .and_then(|c| RawSuffix::try_from(&c).ok()) + .map(|raw| (raw, with_i)); + match (suffix, last) { + (Some(_), _) => {} + (None, Some(c)) if c.is_ascii_digit() && !with_i => {} + _ => return Err(translate!("numfmt-error-invalid-number", "input" => s.quote())), + } - let suffix_len = match suffix { - None => 0, - Some((_, false)) => 1, - Some((_, true)) => 2, - }; + let suffix_len = suffix.map_or(0, |(_, with_i)| 1 + usize::from(with_i)); let number_part = &trimmed[..trimmed.len() - suffix_len]; From 17df0207770d2ecd28a128d4193ac9281ce5dad1 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 5 Apr 2026 10:26:04 +0200 Subject: [PATCH 238/264] numfmt: simplify remove_suffix with base index lookup --- src/uu/numfmt/src/format.rs | 40 ++++++++++--------------------------- 1 file changed, 10 insertions(+), 30 deletions(-) diff --git a/src/uu/numfmt/src/format.rs b/src/uu/numfmt/src/format.rs index d9c929a1ed5..51b029bc089 100644 --- a/src/uu/numfmt/src/format.rs +++ b/src/uu/numfmt/src/format.rs @@ -343,40 +343,20 @@ fn parse_implicit_precision(s: &str) -> usize { } fn remove_suffix(i: f64, s: Option, u: Unit) -> Result { - match (s, u) { - (Some((raw_suffix, false)), Unit::Auto | Unit::Si) => match raw_suffix { - RawSuffix::K => Ok(i * 1e3), - RawSuffix::M => Ok(i * 1e6), - RawSuffix::G => Ok(i * 1e9), - RawSuffix::T => Ok(i * 1e12), - RawSuffix::P => Ok(i * 1e15), - RawSuffix::E => Ok(i * 1e18), - RawSuffix::Z => Ok(i * 1e21), - RawSuffix::Y => Ok(i * 1e24), - RawSuffix::R => Ok(i * 1e27), - RawSuffix::Q => Ok(i * 1e30), - }, - (Some((raw_suffix, false)), Unit::Iec(false)) - | (Some((raw_suffix, true)), Unit::Auto | Unit::Iec(true)) => match raw_suffix { - RawSuffix::K => Ok(i * IEC_BASES[1]), - RawSuffix::M => Ok(i * IEC_BASES[2]), - RawSuffix::G => Ok(i * IEC_BASES[3]), - RawSuffix::T => Ok(i * IEC_BASES[4]), - RawSuffix::P => Ok(i * IEC_BASES[5]), - RawSuffix::E => Ok(i * IEC_BASES[6]), - RawSuffix::Z => Ok(i * IEC_BASES[7]), - RawSuffix::Y => Ok(i * IEC_BASES[8]), - RawSuffix::R => Ok(i * IEC_BASES[9]), - RawSuffix::Q => Ok(i * IEC_BASES[10]), - }, - (Some((raw_suffix, false)), Unit::Iec(true)) => Err( + let Some((raw_suffix, with_i)) = s else { + return Ok(i); + }; + let idx = raw_suffix.index() + 1; + match (with_i, u) { + (false, Unit::Auto | Unit::Si) => Ok(i * SI_BASES[idx]), + (false, Unit::Iec(false)) | (true, Unit::Auto | Unit::Iec(true)) => Ok(i * IEC_BASES[idx]), + (false, Unit::Iec(true)) => Err( translate!("numfmt-error-missing-i-suffix", "number" => i, "suffix" => format!("{raw_suffix:?}")), ), - (Some((raw_suffix, with_i)), Unit::None) => Err( + (_, Unit::None) => Err( translate!("numfmt-error-rejecting-suffix", "number" => i, "suffix" => format!("{raw_suffix:?}{}", if with_i { "i" } else { "" })), ), - (None, _) => Ok(i), - (_, _) => Err(translate!("numfmt-error-suffix-unsupported-for-unit")), + _ => Err(translate!("numfmt-error-suffix-unsupported-for-unit")), } } From 70bb4c354b2fee84b7ced3ed85e795a459ce4ee6 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 5 Apr 2026 10:26:57 +0200 Subject: [PATCH 239/264] numfmt: avoid String allocation in parse_unit_size --- src/uu/numfmt/src/numfmt.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/uu/numfmt/src/numfmt.rs b/src/uu/numfmt/src/numfmt.rs index 3fc22b8ab94..7cd035e2fca 100644 --- a/src/uu/numfmt/src/numfmt.rs +++ b/src/uu/numfmt/src/numfmt.rs @@ -164,15 +164,16 @@ fn parse_unit(s: &str) -> Result { /// Parses a unit size. Suffixes are turned into their integer representations. For example, 'K' /// will return `Ok(1000)`, and '2K' will return `Ok(2000)`. fn parse_unit_size(s: &str) -> Result { - let number: String = s.chars().take_while(char::is_ascii_digit).collect(); - let suffix = &s[number.len()..]; + let split = s.find(|c: char| !c.is_ascii_digit()).unwrap_or(s.len()); + let (number, suffix) = s.split_at(split); - if number.is_empty() || "0".repeat(number.len()) != number { + // Reject all-zero numeric parts like "0" or "00K". + let all_zero = !number.is_empty() && number.bytes().all(|b| b == b'0'); + if !all_zero { if let Some(multiplier) = parse_unit_size_suffix(suffix) { if number.is_empty() { return Ok(multiplier); } - if let Ok(n) = number.parse::() { return Ok(n * multiplier); } From bfdab4fe64123a653896d07894f6a5ff0b28f9b6 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 5 Apr 2026 10:28:01 +0200 Subject: [PATCH 240/264] numfmt: simplify parse_unit_size_suffix --- src/uu/numfmt/src/numfmt.rs | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/uu/numfmt/src/numfmt.rs b/src/uu/numfmt/src/numfmt.rs index 7cd035e2fca..b064a5e6e98 100644 --- a/src/uu/numfmt/src/numfmt.rs +++ b/src/uu/numfmt/src/numfmt.rs @@ -194,20 +194,15 @@ fn parse_unit_size_suffix(s: &str) -> Option { return Some(1); } - let suffix = s.chars().next().unwrap(); - - if let Some(i) = ['K', 'M', 'G', 'T', 'P', 'E'] + let i = ['K', 'M', 'G', 'T', 'P', 'E'] .iter() - .position(|&ch| ch == suffix) - { - return match s.len() { - 1 => Some(SI_BASES[i + 1] as usize), - 2 if s.ends_with('i') => Some(IEC_BASES[i + 1] as usize), - _ => None, - }; - } + .position(|&ch| s.starts_with(ch))?; - None + match s.len() { + 1 => Some(SI_BASES[i + 1] as usize), + 2 if s.ends_with('i') => Some(IEC_BASES[i + 1] as usize), + _ => None, + } } /// Parse delimiter argument, ensuring it's a single character. From 379e96c0fea9b556485743a9fe191e4886cba764 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 5 Apr 2026 10:31:15 +0200 Subject: [PATCH 241/264] uucore: expose SI_BASES and IEC_BASES constants --- .../src/lib/features/parser/parse_size.rs | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/uucore/src/lib/features/parser/parse_size.rs b/src/uucore/src/lib/features/parser/parse_size.rs index cc7e5e9e2e4..15a6d58b8c5 100644 --- a/src/uucore/src/lib/features/parser/parse_size.rs +++ b/src/uucore/src/lib/features/parser/parse_size.rs @@ -6,6 +6,42 @@ //! Parser for sizes in SI or IEC units (multiples of 1000 or 1024 bytes). +/// The first eleven powers of 1000: `1, 10^3, 10^6, ..., 10^30`. +/// +/// Index `n` is the SI base for the `n`-th suffix (0 → no suffix, 1 → K/kB, +/// 2 → M/MB, ...). +pub const SI_BASES: [u128; 11] = [ + 1, + 1_000, + 1_000_000, + 1_000_000_000, + 1_000_000_000_000, + 1_000_000_000_000_000, + 1_000_000_000_000_000_000, + 1_000_000_000_000_000_000_000, + 1_000_000_000_000_000_000_000_000, + 1_000_000_000_000_000_000_000_000_000, + 1_000_000_000_000_000_000_000_000_000_000, +]; + +/// The first eleven powers of 1024: `1, 1024, 1024^2, ..., 1024^10`. +/// +/// Index `n` is the IEC base for the `n`-th suffix (0 → no suffix, 1 → Ki, +/// 2 → Mi, ...). +pub const IEC_BASES: [u128; 11] = [ + 1, + 1_024, + 1_048_576, + 1_073_741_824, + 1_099_511_627_776, + 1_125_899_906_842_624, + 1_152_921_504_606_846_976, + 1_180_591_620_717_411_303_424, + 1_208_925_819_614_629_174_706_176, + 1_237_940_039_285_380_274_899_124_224, + 1_267_650_600_228_229_401_496_703_205_376, +]; + use std::error::Error; use std::fmt; use std::num::{IntErrorKind, ParseIntError}; From d3804cc7ce399f743e5fbe7a0511e671db01d821 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 5 Apr 2026 10:33:50 +0200 Subject: [PATCH 242/264] numfmt: source SI/IEC bases from uucore::parser::parse_size --- src/uu/numfmt/src/format.rs | 19 ++++++++++++------- src/uu/numfmt/src/numfmt.rs | 2 +- src/uu/numfmt/src/units.rs | 26 +++++++++++--------------- 3 files changed, 24 insertions(+), 23 deletions(-) diff --git a/src/uu/numfmt/src/format.rs b/src/uu/numfmt/src/format.rs index 51b029bc089..46f7336e5ee 100644 --- a/src/uu/numfmt/src/format.rs +++ b/src/uu/numfmt/src/format.rs @@ -10,7 +10,9 @@ use uucore::i18n::decimal::locale_grouping_separator; use uucore::translate; use crate::options::{NumfmtOptions, RoundMethod, TransformOptions}; -use crate::units::{DisplayableSuffix, IEC_BASES, RawSuffix, Result, SI_BASES, Suffix, Unit}; +use crate::units::{ + DisplayableSuffix, RawSuffix, Result, Suffix, Unit, iec_bases_f64, si_bases_f64, +}; fn find_numeric_beginning(s: &str) -> Option<&str> { let mut decimal_point_seen = false; @@ -348,8 +350,10 @@ fn remove_suffix(i: f64, s: Option, u: Unit) -> Result { }; let idx = raw_suffix.index() + 1; match (with_i, u) { - (false, Unit::Auto | Unit::Si) => Ok(i * SI_BASES[idx]), - (false, Unit::Iec(false)) | (true, Unit::Auto | Unit::Iec(true)) => Ok(i * IEC_BASES[idx]), + (false, Unit::Auto | Unit::Si) => Ok(i * si_bases_f64()[idx]), + (false, Unit::Iec(false)) | (true, Unit::Auto | Unit::Iec(true)) => { + Ok(i * iec_bases_f64()[idx]) + } (false, Unit::Iec(true)) => Err( translate!("numfmt-error-missing-i-suffix", "number" => i, "suffix" => format!("{raw_suffix:?}")), ), @@ -440,8 +444,8 @@ fn consider_suffix( let suffixes = [K, M, G, T, P, E, Z, Y, R, Q]; let (bases, with_i) = match u { - Unit::Si => (&SI_BASES, false), - Unit::Iec(with_i) => (&IEC_BASES, with_i), + Unit::Si => (si_bases_f64(), false), + Unit::Iec(with_i) => (iec_bases_f64(), with_i), Unit::Auto => return Err(translate!("numfmt-error-unit-auto-not-supported-with-to")), Unit::None => return Ok((n, None)), }; @@ -850,13 +854,14 @@ mod tests { assert!(result.is_ok()); assert_eq!(result.unwrap(), 1e27); + let iec = iec_bases_f64(); let result = remove_suffix(1.0, Some((RawSuffix::Q, true)), Unit::Iec(true)); assert!(result.is_ok()); - assert_eq!(result.unwrap(), IEC_BASES[10]); + assert_eq!(result.unwrap(), iec[10]); let result = remove_suffix(1.0, Some((RawSuffix::R, true)), Unit::Iec(true)); assert!(result.is_ok()); - assert_eq!(result.unwrap(), IEC_BASES[9]); + assert_eq!(result.unwrap(), iec[9]); } #[test] diff --git a/src/uu/numfmt/src/numfmt.rs b/src/uu/numfmt/src/numfmt.rs index b064a5e6e98..775c0b4cc0e 100644 --- a/src/uu/numfmt/src/numfmt.rs +++ b/src/uu/numfmt/src/numfmt.rs @@ -18,10 +18,10 @@ use std::ffi::OsString; use std::io::{BufRead, Write as _, stderr}; use std::str::FromStr; -use units::{IEC_BASES, SI_BASES}; use uucore::display::Quotable; use uucore::error::UResult; use uucore::i18n::decimal::locale_grouping_separator; +use uucore::parser::parse_size::{IEC_BASES, SI_BASES}; use uucore::parser::shortcut_value_parser::ShortcutValueParser; use uucore::ranges::Range; use uucore::{format_usage, os_str_as_bytes, show, translate}; diff --git a/src/uu/numfmt/src/units.rs b/src/uu/numfmt/src/units.rs index 43bf9d10984..45cacffb703 100644 --- a/src/uu/numfmt/src/units.rs +++ b/src/uu/numfmt/src/units.rs @@ -3,22 +3,18 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. use std::fmt; +use uucore::parser::parse_size::{IEC_BASES, SI_BASES}; -pub const SI_BASES: [f64; 11] = [1., 1e3, 1e6, 1e9, 1e12, 1e15, 1e18, 1e21, 1e24, 1e27, 1e30]; +/// `f64` view of [`uucore::parser::parse_size::SI_BASES`] for numfmt's +/// floating-point math paths. +pub fn si_bases_f64() -> [f64; 11] { + SI_BASES.map(|b| b as f64) +} -pub const IEC_BASES: [f64; 11] = [ - 1., - 1_024., - 1_048_576., - 1_073_741_824., - 1_099_511_627_776., - 1_125_899_906_842_624., - 1_152_921_504_606_846_976., - 1_180_591_620_717_411_303_424., - 1_208_925_819_614_629_174_706_176., - 1_237_940_039_285_380_274_899_124_224., - 1_267_650_600_228_229_401_496_703_205_376., -]; +/// `f64` view of [`uucore::parser::parse_size::IEC_BASES`]. +pub fn iec_bases_f64() -> [f64; 11] { + IEC_BASES.map(|b| b as f64) +} pub type WithI = bool; @@ -48,7 +44,7 @@ pub enum RawSuffix { } impl RawSuffix { - /// Index of this suffix in [`SI_BASES`] / [`IEC_BASES`] minus one. + /// Index of this suffix in the base arrays, minus one. /// `K` is 0, `M` is 1, ..., `Q` is 9. The associated base is /// `BASES[self.index() + 1]`. pub fn index(self) -> usize { From 4f00102d8ba435ebf9ebc5c73e39aa92a905339c Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 5 Apr 2026 10:34:45 +0200 Subject: [PATCH 243/264] dd: source SI/IEC bases from uucore::parser::parse_size --- src/uu/dd/src/numbers.rs | 29 +---------------------------- 1 file changed, 1 insertion(+), 28 deletions(-) diff --git a/src/uu/dd/src/numbers.rs b/src/uu/dd/src/numbers.rs index f718052015c..36d141f66e6 100644 --- a/src/uu/dd/src/numbers.rs +++ b/src/uu/dd/src/numbers.rs @@ -5,36 +5,9 @@ //! Functions for formatting a number as a magnitude and a unit suffix. -/// The first ten powers of 1024. -const IEC_BASES: [u128; 10] = [ - 1, - 1_024, - 1_048_576, - 1_073_741_824, - 1_099_511_627_776, - 1_125_899_906_842_624, - 1_152_921_504_606_846_976, - 1_180_591_620_717_411_303_424, - 1_208_925_819_614_629_174_706_176, - 1_237_940_039_285_380_274_899_124_224, -]; +use uucore::parser::parse_size::{IEC_BASES, SI_BASES}; const IEC_SUFFIXES: [&str; 9] = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"]; - -/// The first ten powers of 1000. -const SI_BASES: [u128; 10] = [ - 1, - 1_000, - 1_000_000, - 1_000_000_000, - 1_000_000_000_000, - 1_000_000_000_000_000, - 1_000_000_000_000_000_000, - 1_000_000_000_000_000_000_000, - 1_000_000_000_000_000_000_000_000, - 1_000_000_000_000_000_000_000_000_000, -]; - const SI_SUFFIXES: [&str; 9] = ["B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; /// A `SuffixType` determines whether the suffixes are 1000 or 1024 based. From af7360ca2c80a5c5b45767f4d23fd7feb58a1e5e Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 5 Apr 2026 10:36:01 +0200 Subject: [PATCH 244/264] df: source SI/IEC bases from uucore::parser::parse_size --- src/uu/df/src/blocks.rs | 36 +++++------------------------------- 1 file changed, 5 insertions(+), 31 deletions(-) diff --git a/src/uu/df/src/blocks.rs b/src/uu/df/src/blocks.rs index 24ebbf44225..16f0e840200 100644 --- a/src/uu/df/src/blocks.rs +++ b/src/uu/df/src/blocks.rs @@ -9,37 +9,11 @@ use std::{env, fmt}; use uucore::{ display::Quotable, - parser::parse_size::{ParseSizeError, parse_size_non_zero_u64, parse_size_u64}, + parser::parse_size::{ + IEC_BASES, ParseSizeError, SI_BASES, parse_size_non_zero_u64, parse_size_u64, + }, }; -/// The first ten powers of 1024. -const IEC_BASES: [u128; 10] = [ - 1, - 1_024, - 1_048_576, - 1_073_741_824, - 1_099_511_627_776, - 1_125_899_906_842_624, - 1_152_921_504_606_846_976, - 1_180_591_620_717_411_303_424, - 1_208_925_819_614_629_174_706_176, - 1_237_940_039_285_380_274_899_124_224, -]; - -/// The first ten powers of 1000. -const SI_BASES: [u128; 10] = [ - 1, - 1_000, - 1_000_000, - 1_000_000_000, - 1_000_000_000_000, - 1_000_000_000_000_000, - 1_000_000_000_000_000_000, - 1_000_000_000_000_000_000_000, - 1_000_000_000_000_000_000_000_000, - 1_000_000_000_000_000_000_000_000_000, -]; - /// A `SuffixType` determines whether the suffixes are 1000 or 1024 based, and whether they are /// intended for `HumanReadable` mode or not. #[derive(Clone, Copy)] @@ -50,8 +24,8 @@ pub(crate) enum SuffixType { } impl SuffixType { - /// The first ten powers of 1024 and 1000, respectively. - fn bases(self) -> [u128; 10] { + /// The first eleven powers of 1024 and 1000, respectively. + fn bases(self) -> [u128; 11] { match self { Self::Iec | Self::HumanReadable(HumanReadable::Binary) => IEC_BASES, Self::Si | Self::HumanReadable(HumanReadable::Decimal) => SI_BASES, From 55ee7828a588fa725d983def0bc7d45ff76fde4e Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 5 Apr 2026 21:16:31 +0200 Subject: [PATCH 245/264] =?UTF-8?q?fr-FR:=20fix=20anglicisms=20(support?= =?UTF-8?q?=C3=A9=E2=86=92pris=20en=20charge,=20drapeau=E2=86=92option,=20?= =?UTF-8?q?removed=E2=86=92supprim=C3=A9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/uu/cp/locales/fr-FR.ftl | 10 +++++----- src/uu/cut/locales/fr-FR.ftl | 2 +- src/uu/dd/locales/fr-FR.ftl | 2 +- src/uu/du/locales/fr-FR.ftl | 4 ++-- src/uu/env/locales/fr-FR.ftl | 2 +- src/uu/fmt/locales/fr-FR.ftl | 2 +- src/uu/head/locales/fr-FR.ftl | 2 +- src/uu/ls/locales/fr-FR.ftl | 2 +- src/uu/mv/locales/fr-FR.ftl | 2 +- src/uu/numfmt/locales/fr-FR.ftl | 10 +++++----- src/uu/pinky/locales/fr-FR.ftl | 2 +- src/uu/pr/locales/fr-FR.ftl | 2 +- src/uu/rm/locales/fr-FR.ftl | 2 +- src/uu/split/locales/fr-FR.ftl | 2 +- src/uu/tail/locales/fr-FR.ftl | 4 ++-- src/uu/who/locales/fr-FR.ftl | 2 +- 16 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/uu/cp/locales/fr-FR.ftl b/src/uu/cp/locales/fr-FR.ftl index 76860de0351..4a2ed8baf90 100644 --- a/src/uu/cp/locales/fr-FR.ftl +++ b/src/uu/cp/locales/fr-FR.ftl @@ -44,7 +44,7 @@ cp-help-one-file-system = rester sur ce système de fichiers cp-help-sparse = contrôler la création de fichiers épars. Voir ci-dessous cp-help-selinux = définir le contexte de sécurité SELinux du fichier de destination au type par défaut cp-help-context = comme -Z, ou si CTX est spécifié, définir le contexte de sécurité SELinux ou SMACK à CTX -cp-help-progress = Afficher une barre de progression. Note : cette fonctionnalité n'est pas supportée par GNU coreutils. +cp-help-progress = Afficher une barre de progression. Note : cette fonctionnalité n'est pas prise en charge par GNU coreutils. cp-help-copy-contents = Non implémenté : copier le contenu des fichiers spéciaux lors de la récursion # Messages d'erreur @@ -78,8 +78,8 @@ cp-error-not-all-files-copied = Tous les fichiers n'ont pas été copiés cp-error-reflink-always-sparse-auto = `--reflink=always` ne peut être utilisé qu'avec --sparse=auto cp-error-file-exists = { $path } : Le fichier existe cp-error-invalid-backup-argument = --backup est mutuellement exclusif avec -n ou --update=none-fail -cp-error-reflink-not-supported = --reflink n'est supporté que sur linux et macOS -cp-error-sparse-not-supported = --sparse n'est supporté que sur linux +cp-error-reflink-not-supported = --reflink n'est pris en charge que sur linux et macOS +cp-error-sparse-not-supported = --sparse n'est pris en charge que sur linux cp-error-not-a-directory = { $path } n'est pas un répertoire cp-error-selinux-not-enabled = SELinux n'était pas activé lors de la compilation ! cp-error-selinux-set-context = échec de la définition du contexte de sécurité de { $path } : { $error } @@ -98,7 +98,7 @@ cp-error-backup-format = cp : { $error } cp-debug-enum-no = non cp-debug-enum-yes = oui cp-debug-enum-avoided = évité -cp-debug-enum-unsupported = non supporté +cp-debug-enum-unsupported = non pris en charge cp-debug-enum-unknown = inconnu cp-debug-enum-zeros = zéros cp-debug-enum-seek-hole = SEEK_HOLE @@ -110,7 +110,7 @@ cp-warning-source-specified-more-than-once = { $file_type } source { $source } s # Messages verbeux et de débogage cp-verbose-copied = { $source } -> { $dest } cp-debug-skipped = { $path } ignoré -cp-verbose-removed = removed { $path } +cp-verbose-removed = supprimé { $path } cp-verbose-created-directory = { $source } -> { $dest } cp-debug-copy-offload = copy offload : { $offload }, reflink : { $reflink }, sparse detection : { $sparse } diff --git a/src/uu/cut/locales/fr-FR.ftl b/src/uu/cut/locales/fr-FR.ftl index cab0d8ccd35..a95773099d6 100644 --- a/src/uu/cut/locales/fr-FR.ftl +++ b/src/uu/cut/locales/fr-FR.ftl @@ -70,7 +70,7 @@ cut-after-help = Chaque appel doit spécifier un mode (quoi utiliser pour les co #### Filtrage optionnel basé sur le délimiteur - Si le drapeau --only-delimited (-s) est fourni, seules les lignes qui + Si l'option --only-delimited (-s) est fournie, seules les lignes qui contiennent le délimiteur seront affichées #### Remplacer le délimiteur diff --git a/src/uu/dd/locales/fr-FR.ftl b/src/uu/dd/locales/fr-FR.ftl index 153608174eb..494a284a814 100644 --- a/src/uu/dd/locales/fr-FR.ftl +++ b/src/uu/dd/locales/fr-FR.ftl @@ -51,7 +51,7 @@ dd-after-help = ### Opérandes - noxfer : Afficher les statistiques de volume finales, mais pas les statistiques de performance. - none : N'afficher aucune statistique. - L'affichage des statistiques de performance est aussi déclenché par le signal INFO (quand supporté), + L'affichage des statistiques de performance est aussi déclenché par le signal INFO (quand pris en charge), ou le signal USR1. Définir la variable d'environnement POSIXLY_CORRECT à n'importe quelle valeur (y compris une valeur vide) fera ignorer le signal USR1. diff --git a/src/uu/du/locales/fr-FR.ftl b/src/uu/du/locales/fr-FR.ftl index 52b89145973..e4e55ed0f0f 100644 --- a/src/uu/du/locales/fr-FR.ftl +++ b/src/uu/du/locales/fr-FR.ftl @@ -10,7 +10,7 @@ du-after-help = Les valeurs affichées sont en unités de la première TAILLE di de 1000). Les unités peuvent être décimales, hexadécimales, octales, binaires. MOTIF permet des exclusions avancées. Par exemple, les syntaxes suivantes - sont supportées : + sont prises en charge : ? correspondra à un seul caractère { "*" } correspondra à zéro ou plusieurs caractères {"{"}a,b{"}"} correspondra à a ou b @@ -54,7 +54,7 @@ du-error-invalid-time-style = argument invalide { $style } pour 'style de temps' - 'iso' - +FORMAT (e.g., +%H:%M) pour un format de type 'date' Essayez '{ $help }' pour plus d'informations. -du-error-invalid-time-arg = les arguments 'birth' et 'creation' pour --time ne sont pas supportés sur cette plateforme. +du-error-invalid-time-arg = les arguments 'birth' et 'creation' pour --time ne sont pas pris en charge sur cette plateforme. du-error-invalid-glob = Syntaxe d'exclusion invalide : { $error } du-error-cannot-read-directory = impossible de lire le répertoire { $path } du-error-cannot-access = impossible d'accéder à { $path } diff --git a/src/uu/env/locales/fr-FR.ftl b/src/uu/env/locales/fr-FR.ftl index 2ca1968d230..b3fa0c2fbe6 100644 --- a/src/uu/env/locales/fr-FR.ftl +++ b/src/uu/env/locales/fr-FR.ftl @@ -38,7 +38,7 @@ env-error-cannot-unset = impossible de supprimer '{ $name }' : Argument invalide env-error-cannot-unset-invalid = impossible de supprimer { $name } : Argument invalide env-error-must-specify-command-with-chdir = doit spécifier une commande avec --chdir (-C) env-error-cannot-change-directory = impossible de changer de répertoire vers { $directory } : { $error } -env-error-argv0-not-supported = --argv0 n'est actuellement pas supporté sur cette plateforme +env-error-argv0-not-supported = --argv0 n'est actuellement pas pris en charge sur cette plateforme env-error-permission-denied = { $program } : Permission refusée env-error-unknown = erreur inconnue : { $error } env-error-failed-set-signal-action = échec de la définition de l'action du signal pour le signal { $signal } : { $error } diff --git a/src/uu/fmt/locales/fr-FR.ftl b/src/uu/fmt/locales/fr-FR.ftl index 640942a8fe3..0957a973f53 100644 --- a/src/uu/fmt/locales/fr-FR.ftl +++ b/src/uu/fmt/locales/fr-FR.ftl @@ -4,7 +4,7 @@ fmt-usage = [OPTION]... [FICHIER]... # Messages d'aide fmt-crown-margin-help = La première et la deuxième ligne d'un paragraphe peuvent avoir des indentations différentes, auquel cas l'indentation de la première ligne est préservée, et chaque ligne suivante correspond à l'indentation de la deuxième ligne. fmt-tagged-paragraph-help = Comme -c, sauf que la première et la deuxième ligne d'un paragraphe *doivent* avoir des indentations différentes ou elles sont traitées comme des paragraphes séparés. -fmt-preserve-headers-help = Tente de détecter et préserver les en-têtes de courrier dans l'entrée. Attention en combinant ce drapeau avec -p. +fmt-preserve-headers-help = Tente de détecter et préserver les en-têtes de courrier dans l'entrée. Attention en combinant cette option avec -p. fmt-split-only-help = Divise les lignes seulement, ne les reformate pas. fmt-uniform-spacing-help = Insère exactement un espace entre les mots, et deux entre les phrases. Les fins de phrase dans l'entrée sont détectées comme [?!.] suivies de deux espaces ou d'une nouvelle ligne ; les autres ponctuations ne sont pas interprétées comme des fins de phrase. fmt-prefix-help = Reformate seulement les lignes commençant par PRÉFIXE, en rattachant PRÉFIXE aux lignes reformatées. À moins que -x soit spécifié, les espaces de début seront ignorés lors de la correspondance avec PRÉFIXE. diff --git a/src/uu/head/locales/fr-FR.ftl b/src/uu/head/locales/fr-FR.ftl index 26abb6f8b72..d5851797c26 100644 --- a/src/uu/head/locales/fr-FR.ftl +++ b/src/uu/head/locales/fr-FR.ftl @@ -2,7 +2,7 @@ head-about = Affiche les 10 premières lignes de chaque FICHIER sur la sortie st Avec plus d'un FICHIER, précède chacun d'un en-tête donnant le nom du fichier. Sans FICHIER, ou quand FICHIER est -, lit l'entrée standard. - Les arguments obligatoires pour les drapeaux longs sont obligatoires pour les drapeaux courts aussi. + Les arguments obligatoires pour les options longues sont obligatoires pour les options courtes aussi. head-usage = head [DRAPEAU]... [FICHIER]... # Messages d'aide diff --git a/src/uu/ls/locales/fr-FR.ftl b/src/uu/ls/locales/fr-FR.ftl index bd1da934b90..06090f76393 100644 --- a/src/uu/ls/locales/fr-FR.ftl +++ b/src/uu/ls/locales/fr-FR.ftl @@ -85,7 +85,7 @@ ls-help-dereference-dir-args = Ne pas suivre les liens symboliques sauf quand il donnés comme arguments de ligne de commande. ls-help-dereference-args = Ne pas suivre les liens symboliques sauf quand ils sont donnés comme arguments de ligne de commande. ls-help-no-group = Ne pas afficher le groupe en format long. -ls-help-author = Afficher l'auteur en format long. Sur les plateformes supportées, +ls-help-author = Afficher l'auteur en format long. Sur les plateformes prises en charge, l'auteur correspond toujours au propriétaire du fichier. ls-help-all-files = Ne pas ignorer les fichiers cachés (fichiers dont les noms commencent par '.'). ls-help-almost-all = Dans un répertoire, ne pas ignorer tous les noms de fichiers qui commencent par '.', diff --git a/src/uu/mv/locales/fr-FR.ftl b/src/uu/mv/locales/fr-FR.ftl index 9ea2f2114b1..2c7a279b74b 100644 --- a/src/uu/mv/locales/fr-FR.ftl +++ b/src/uu/mv/locales/fr-FR.ftl @@ -47,7 +47,7 @@ mv-help-target-directory = déplacer tous les arguments SOURCE dans RÉPERTOIRE mv-help-no-target-directory = traiter DEST comme un fichier normal mv-help-verbose = expliquer ce qui est fait mv-help-progress = Afficher une barre de progression. - Note : cette fonctionnalité n'est pas supportée par GNU coreutils. + Note : cette fonctionnalité n'est pas prise en charge par GNU coreutils. mv-help-debug = expliquer comment un fichier est copié. Implique -v # Messages verbeux diff --git a/src/uu/numfmt/locales/fr-FR.ftl b/src/uu/numfmt/locales/fr-FR.ftl index c6eb9b8ab67..71f0ab54063 100644 --- a/src/uu/numfmt/locales/fr-FR.ftl +++ b/src/uu/numfmt/locales/fr-FR.ftl @@ -30,7 +30,7 @@ numfmt-after-help = Options d'UNITÉ : Plusieurs champs/plages peuvent être séparés par des virgules FORMAT doit être adapté pour imprimer un argument à virgule flottante %f. - Une guillemet optionnelle (%'f) activera --grouping (si supporté par la locale actuelle). + Une guillemet optionnelle (%'f) activera --grouping (si pris en charge par la locale actuelle). Une valeur de largeur optionnelle (%10f) remplira la sortie. Un zéro optionnel (%010f) remplira le nombre de zéros. Des valeurs négatives optionnelles (%-10f) aligneront à gauche. Une précision optionnelle (%.1f) remplacera la précision déterminée par l'entrée. @@ -53,7 +53,7 @@ numfmt-help-invalid = définir le mode d'échec pour les entrées invalides numfmt-help-zero-terminated = le délimiteur de ligne est NUL, pas retour à la ligne # Messages d'erreur -numfmt-error-unsupported-unit = Une unité non supportée est spécifiée +numfmt-error-unsupported-unit = Une unité non prise en charge est spécifiée numfmt-error-invalid-unit-size = taille d'unité invalide : { $size } numfmt-error-invalid-padding = valeur de remplissage invalide { $value } numfmt-error-invalid-header = valeur d'en-tête invalide { $value } @@ -66,9 +66,9 @@ numfmt-error-invalid-specific-suffix = suffixe invalide dans l'entrée { $input numfmt-error-invalid-number = nombre invalide : { $input } numfmt-error-missing-i-suffix = suffixe 'i' manquant dans l'entrée : '{ $number }{ $suffix }' (par ex. Ki/Mi/Gi) numfmt-error-rejecting-suffix = rejet du suffixe dans l'entrée : '{ $number }{ $suffix }' (considérez utiliser --from) -numfmt-error-suffix-unsupported-for-unit = Ce suffixe n'est pas supporté pour l'unité spécifiée -numfmt-error-unit-auto-not-supported-with-to = L'unité 'auto' n'est pas supportée avec les options --to -numfmt-error-number-too-big = Le nombre est trop grand et non supporté +numfmt-error-suffix-unsupported-for-unit = Ce suffixe n'est pas pris en charge pour l'unité spécifiée +numfmt-error-unit-auto-not-supported-with-to = L'unité 'auto' n'est pas prise en charge avec les options --to +numfmt-error-number-too-big = Le nombre est trop grand et non pris en charge numfmt-error-format-no-percent = le format '{ $format }' n'a pas de directive % numfmt-error-format-ends-in-percent = le format '{ $format }' se termine par % numfmt-error-invalid-format-directive = format invalide '{ $format }', la directive doit être %[0]['][-][N][.][N]f diff --git a/src/uu/pinky/locales/fr-FR.ftl b/src/uu/pinky/locales/fr-FR.ftl index ee37681c940..81a0b3f3ce8 100644 --- a/src/uu/pinky/locales/fr-FR.ftl +++ b/src/uu/pinky/locales/fr-FR.ftl @@ -39,4 +39,4 @@ pinky-project-label = Projet : pinky-plan-label = Plan # Messages de statut -pinky-unsupported-openbsd = commande non supportée sur OpenBSD +pinky-unsupported-openbsd = commande non prise en charge sur OpenBSD diff --git a/src/uu/pr/locales/fr-FR.ftl b/src/uu/pr/locales/fr-FR.ftl index 28c6ab8f0bc..e9c11214f44 100644 --- a/src/uu/pr/locales/fr-FR.ftl +++ b/src/uu/pr/locales/fr-FR.ftl @@ -95,7 +95,7 @@ pr-try-help-message = Essayez 'pr --help' pour plus d'informations. pr-error-reading-input = pr : La lecture depuis l'entrée {$file} a donné une erreur pr-error-unknown-filetype = pr : {$file} : type de fichier inconnu pr-error-is-directory = pr : {$file} : Est un répertoire -pr-error-socket-not-supported = pr : impossible d'ouvrir {$file}, Opération non supportée sur socket +pr-error-socket-not-supported = pr : impossible d'ouvrir {$file}, Opération non prise en charge sur socket pr-error-no-such-file = pr : impossible d'ouvrir {$file}, Aucun fichier ou répertoire de ce type pr-error-column-merge-conflict = impossible de spécifier le nombre de colonnes lors de l'impression en parallèle pr-error-across-merge-conflict = impossible de spécifier à la fois l'impression transversale et l'impression en parallèle diff --git a/src/uu/rm/locales/fr-FR.ftl b/src/uu/rm/locales/fr-FR.ftl index 9c9124db676..abbe7b8eadf 100644 --- a/src/uu/rm/locales/fr-FR.ftl +++ b/src/uu/rm/locales/fr-FR.ftl @@ -28,7 +28,7 @@ rm-help-preserve-root = ne pas supprimer '/' (par défaut) rm-help-recursive = supprimer les répertoires et leur contenu récursivement rm-help-dir = supprimer les répertoires vides rm-help-verbose = expliquer ce qui est fait -rm-help-progress = afficher une barre de progression. Note : cette fonctionnalité n'est pas supportée par GNU coreutils. +rm-help-progress = afficher une barre de progression. Note : cette fonctionnalité n'est pas prise en charge par GNU coreutils. # Messages de progression rm-progress-removing = Suppression diff --git a/src/uu/split/locales/fr-FR.ftl b/src/uu/split/locales/fr-FR.ftl index e8e70c280fa..832e923a4fd 100644 --- a/src/uu/split/locales/fr-FR.ftl +++ b/src/uu/split/locales/fr-FR.ftl @@ -23,7 +23,7 @@ split-error-multi-character-separator = séparateur multi-caractères { $separat split-error-multiple-separator-characters = plusieurs caractères de séparateur spécifiés split-error-filter-with-kth-chunk = --filter ne traite pas un chunk extrait vers stdout split-error-invalid-io-block-size = taille de bloc IO invalide : { $size } -split-error-not-supported = --filter n'est actuellement pas supporté sur cette plateforme +split-error-not-supported = --filter n'est actuellement pas pris en charge sur cette plateforme split-error-invalid-number-of-chunks = nombre de chunks invalide : { $chunks } split-error-invalid-chunk-number = numéro de chunk invalide : { $chunk } split-error-invalid-number-of-lines = nombre de lignes invalide : { $error } diff --git a/src/uu/tail/locales/fr-FR.ftl b/src/uu/tail/locales/fr-FR.ftl index 85d973571ae..72cb4108108 100644 --- a/src/uu/tail/locales/fr-FR.ftl +++ b/src/uu/tail/locales/fr-FR.ftl @@ -1,8 +1,8 @@ tail-about = Afficher les 10 dernières lignes de chaque FICHIER sur la sortie standard. Avec plus d'un FICHIER, précéder chacun d'un en-tête donnant le nom du fichier. Sans FICHIER, ou quand FICHIER est -, lire l'entrée standard. - Les arguments obligatoires pour les drapeaux longs sont également obligatoires pour les drapeaux courts. -tail-usage = tail [DRAPEAU]... [FICHIER]... + Les arguments obligatoires pour les options longues sont également obligatoires pour les options courtes. +tail-usage = tail [OPTION]... [FICHIER]... # Messages d'aide tail-help-bytes = Nombre d'octets à afficher diff --git a/src/uu/who/locales/fr-FR.ftl b/src/uu/who/locales/fr-FR.ftl index ca88a4946c1..27d0230182c 100644 --- a/src/uu/who/locales/fr-FR.ftl +++ b/src/uu/who/locales/fr-FR.ftl @@ -56,4 +56,4 @@ who-heading-exit = SORTIE who-canonicalize-error = échec de canonicalisation de { $host } # Platform-specific messages -who-unsupported-openbsd = commande non supportée sur OpenBSD +who-unsupported-openbsd = commande non prise en charge sur OpenBSD From 0b0b8350db62eefc67bd1e34bb68df7ed94b5d8b Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 5 Apr 2026 21:11:30 +0200 Subject: [PATCH 246/264] env: hardcode config file error message --- src/uu/env/locales/en-US.ftl | 1 - src/uu/env/locales/fr-FR.ftl | 1 - src/uu/env/src/env.rs | 8 ++------ 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/uu/env/locales/en-US.ftl b/src/uu/env/locales/en-US.ftl index a460c69fba4..d2cdd15d140 100644 --- a/src/uu/env/locales/en-US.ftl +++ b/src/uu/env/locales/en-US.ftl @@ -28,7 +28,6 @@ env-error-unexpected-number = Unexpected character: '{ $char }', expected variab env-error-expected-brace-or-colon = Unexpected character: '{ $char }', expected a closing brace ('{"}"}') or colon (':') at position { $position } env-error-cannot-specify-null-with-command = cannot specify --null (-0) with command env-error-invalid-signal = { $signal }: invalid signal -env-error-config-file = { $file }: { $error } env-error-variable-name-issue = variable name issue (at { $position }): { $error } env-error-generic = Error: { $error } env-error-no-such-file = { $program }: No such file or directory diff --git a/src/uu/env/locales/fr-FR.ftl b/src/uu/env/locales/fr-FR.ftl index b3fa0c2fbe6..78351e0da4b 100644 --- a/src/uu/env/locales/fr-FR.ftl +++ b/src/uu/env/locales/fr-FR.ftl @@ -29,7 +29,6 @@ env-error-expected-brace-or-colon = Caractère inattendu : '{ $char }', accolade env-error-cannot-specify-null-with-command = impossible de spécifier --null (-0) avec une commande env-error-invalid-signal = { $signal } : signal invalide -env-error-config-file = { $file } : { $error } env-error-variable-name-issue = problème de nom de variable (à { $position }) : { $error } env-error-generic = Erreur : { $error } env-error-no-such-file = { $program } : Aucun fichier ou répertoire de ce type diff --git a/src/uu/env/src/env.rs b/src/uu/env/src/env.rs index 7b36c7c6355..3343544253e 100644 --- a/src/uu/env/src/env.rs +++ b/src/uu/env/src/env.rs @@ -319,12 +319,8 @@ fn load_config_file(opts: &mut Options) -> UResult<()> { Ini::load_from_file(file) }; - let conf = conf.map_err(|e| { - USimpleError::new( - 1, - translate!("env-error-config-file", "file" => file.maybe_quote(), "error" => e), - ) - })?; + let conf = + conf.map_err(|e| USimpleError::new(1, format!("{}: {e}", file.maybe_quote())))?; for (_, prop) in &conf { // ignore all INI section lines (treat them as comments) From 664e54bfb6be2207eb423fcc3b04c6779efef45e Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 5 Apr 2026 21:11:34 +0200 Subject: [PATCH 247/264] cp: hardcode verbose copy/created-directory message --- src/uu/cp/locales/en-US.ftl | 2 -- src/uu/cp/locales/fr-FR.ftl | 2 -- src/uu/cp/src/cp.rs | 5 +---- 3 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/uu/cp/locales/en-US.ftl b/src/uu/cp/locales/en-US.ftl index 36ab60f25f0..7c47a91b4d2 100644 --- a/src/uu/cp/locales/en-US.ftl +++ b/src/uu/cp/locales/en-US.ftl @@ -109,10 +109,8 @@ cp-debug-enum-seek-hole-zeros = SEEK_HOLE + zeros cp-warning-source-specified-more-than-once = source { $file_type } { $source } specified more than once # Verbose and debug messages -cp-verbose-copied = { $source } -> { $dest } cp-debug-skipped = skipped { $path } cp-verbose-removed = removed { $path } -cp-verbose-created-directory = { $source } -> { $dest } cp-debug-copy-offload = copy offload: { $offload }, reflink: { $reflink }, sparse detection: { $sparse } # Prompts diff --git a/src/uu/cp/locales/fr-FR.ftl b/src/uu/cp/locales/fr-FR.ftl index 4a2ed8baf90..b9d7450016b 100644 --- a/src/uu/cp/locales/fr-FR.ftl +++ b/src/uu/cp/locales/fr-FR.ftl @@ -108,10 +108,8 @@ cp-debug-enum-seek-hole-zeros = SEEK_HOLE + zéros cp-warning-source-specified-more-than-once = { $file_type } source { $source } spécifié plus d'une fois # Messages verbeux et de débogage -cp-verbose-copied = { $source } -> { $dest } cp-debug-skipped = { $path } ignoré cp-verbose-removed = supprimé { $path } -cp-verbose-created-directory = { $source } -> { $dest } cp-debug-copy-offload = copy offload : { $offload }, reflink : { $reflink }, sparse detection : { $sparse } # Invites diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index c3ca44e7696..9f023a3937f 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -2213,10 +2213,7 @@ fn print_paths(parents: bool, source: &Path, dest: &Path) { // a/b -> d/a/b // for (x, y) in aligned_ancestors(source, dest) { - println!( - "{}", - translate!("cp-verbose-created-directory", "source" => x.display(), "dest" => y.display()) - ); + println!("{} -> {}", x.display(), y.display()); } } From 340d62d6491a57f88753c84ef4496003f1432f61 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 5 Apr 2026 21:11:37 +0200 Subject: [PATCH 248/264] install: hardcode verbose copy message --- src/uu/install/locales/en-US.ftl | 1 - src/uu/install/locales/fr-FR.ftl | 1 - src/uu/install/src/install.rs | 6 +----- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/src/uu/install/locales/en-US.ftl b/src/uu/install/locales/en-US.ftl index a9a0d5395d7..34189d86333 100644 --- a/src/uu/install/locales/en-US.ftl +++ b/src/uu/install/locales/en-US.ftl @@ -57,5 +57,4 @@ install-warning-compare-ignored = the --compare (-C) option is ignored when you install-verbose-creating-directory = creating directory { $path } install-verbose-creating-directory-step = install: creating directory { $path } install-verbose-removed = removed { $path } -install-verbose-copy = { $from } -> { $to } install-verbose-backup = (backup: { $backup }) diff --git a/src/uu/install/locales/fr-FR.ftl b/src/uu/install/locales/fr-FR.ftl index 3d5f2feddee..054cc292eb2 100644 --- a/src/uu/install/locales/fr-FR.ftl +++ b/src/uu/install/locales/fr-FR.ftl @@ -57,5 +57,4 @@ install-warning-compare-ignored = l'option --compare (-C) est ignorée quand un install-verbose-creating-directory = création du répertoire { $path } install-verbose-creating-directory-step = install : création du répertoire { $path } install-verbose-removed = supprimé { $path } -install-verbose-copy = { $from } -> { $to } install-verbose-backup = (sauvegarde : { $backup }) diff --git a/src/uu/install/src/install.rs b/src/uu/install/src/install.rs index 596d9ebf35d..f19f8aca249 100644 --- a/src/uu/install/src/install.rs +++ b/src/uu/install/src/install.rs @@ -1106,11 +1106,7 @@ fn finalize_installed_file( } if b.verbose { - write!( - stdout(), - "{}", - translate!("install-verbose-copy", "from" => from.quote(), "to" => to.quote()) - )?; + write!(stdout(), "{} -> {}", from.quote(), to.quote())?; match backup_path { Some(path) => writeln!( stdout(), From 9913be82fc1000bebe0fa4dc751a69ec860ea9ea Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 5 Apr 2026 21:11:41 +0200 Subject: [PATCH 249/264] tail: hardcode no-such-file and became-inaccessible status messages --- src/uu/tail/locales/en-US.ftl | 2 -- src/uu/tail/locales/fr-FR.ftl | 2 -- src/uu/tail/src/follow/watch.rs | 11 +++++++---- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/uu/tail/locales/en-US.ftl b/src/uu/tail/locales/en-US.ftl index 6d434ae9862..44b9a7c6288 100644 --- a/src/uu/tail/locales/en-US.ftl +++ b/src/uu/tail/locales/en-US.ftl @@ -58,10 +58,8 @@ tail-status-has-been-replaced-following-new-file = { $file } has been replaced; tail-status-file-truncated = { $file }: file truncated tail-status-replaced-with-untailable-file = { $file } has been replaced with an untailable file tail-status-replaced-with-untailable-file-giving-up = { $file } has been replaced with an untailable file; giving up on this name -tail-status-file-became-inaccessible = { $file } { $become_inaccessible }: { $no_such_file } tail-status-directory-containing-watched-file-removed = directory containing watched file was removed tail-status-backend-cannot-be-used-reverting-to-polling = { $backend } cannot be used, reverting to polling -tail-status-file-no-such-file = { $file }: { $no_such_file } # Text constants tail-bad-fd = Bad file descriptor diff --git a/src/uu/tail/locales/fr-FR.ftl b/src/uu/tail/locales/fr-FR.ftl index 72cb4108108..39f0a5a7ebb 100644 --- a/src/uu/tail/locales/fr-FR.ftl +++ b/src/uu/tail/locales/fr-FR.ftl @@ -57,10 +57,8 @@ tail-status-has-been-replaced-following-new-file = { $file } a été remplacé ; tail-status-file-truncated = { $file } : fichier tronqué tail-status-replaced-with-untailable-file = { $file } a été remplacé par un fichier non suivable tail-status-replaced-with-untailable-file-giving-up = { $file } a été remplacé par un fichier non suivable ; abandon de ce nom -tail-status-file-became-inaccessible = { $file } { $become_inaccessible } : { $no_such_file } tail-status-directory-containing-watched-file-removed = le répertoire contenant le fichier surveillé a été supprimé tail-status-backend-cannot-be-used-reverting-to-polling = { $backend } ne peut pas être utilisé, retour au sondage -tail-status-file-no-such-file = { $file } : { $no_such_file } # Constantes de texte tail-bad-fd = Descripteur de fichier incorrect diff --git a/src/uu/tail/src/follow/watch.rs b/src/uu/tail/src/follow/watch.rs index b3f88c224a3..592c866dbf9 100644 --- a/src/uu/tail/src/follow/watch.rs +++ b/src/uu/tail/src/follow/watch.rs @@ -394,8 +394,10 @@ impl Observer { if let Some(old_md) = self.files.get_mut_metadata(event_path) { if old_md.is_tailable() && self.files.get(event_path).reader.is_some() { show_error!( - "{}", - translate!("tail-status-file-became-inaccessible", "file" => display_name.quote(), "become_inaccessible" => translate!("tail-become-inaccessible"), "no_such_file" => translate!("tail-no-such-file-or-directory")) + "{} {}: {}", + display_name.quote(), + translate!("tail-become-inaccessible"), + translate!("tail-no-such-file-or-directory") ); } } @@ -410,8 +412,9 @@ impl Observer { } } else { show_error!( - "{}", - translate!("tail-status-file-no-such-file", "file" => display_name, "no_such_file" => translate!("tail-no-such-file-or-directory")) + "{}: {}", + display_name, + translate!("tail-no-such-file-or-directory") ); if !self.files.files_remaining() && self.use_polling { // NOTE: GNU's tail exits here for `---disable-inotify` From 09d7fe7178b1a4b0356fc23aa9f96f34a74df15e Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 5 Apr 2026 21:11:44 +0200 Subject: [PATCH 250/264] who: hardcode idle current/unknown placeholders --- src/uu/who/locales/en-US.ftl | 2 -- src/uu/who/locales/fr-FR.ftl | 1 - src/uu/who/src/platform/unix.rs | 2 +- 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/uu/who/locales/en-US.ftl b/src/uu/who/locales/en-US.ftl index f092489a16a..2984c2fdcc5 100644 --- a/src/uu/who/locales/en-US.ftl +++ b/src/uu/who/locales/en-US.ftl @@ -32,9 +32,7 @@ who-user-count = # { $count -> } # Idle time indicators -who-idle-current = . who-idle-old = old -who-idle-unknown = ? # System information who-runlevel = run-level { $level } diff --git a/src/uu/who/locales/fr-FR.ftl b/src/uu/who/locales/fr-FR.ftl index 27d0230182c..0c853e51ee9 100644 --- a/src/uu/who/locales/fr-FR.ftl +++ b/src/uu/who/locales/fr-FR.ftl @@ -32,7 +32,6 @@ who-user-count = # { $count -> # Idle time indicators who-idle-old = anc. -who-idle-unknown = ? # System information who-runlevel = niveau-exec { $level } diff --git a/src/uu/who/src/platform/unix.rs b/src/uu/who/src/platform/unix.rs index 6fb70753dd0..975ac31c4df 100644 --- a/src/uu/who/src/platform/unix.rs +++ b/src/uu/who/src/platform/unix.rs @@ -379,7 +379,7 @@ impl Who { } let idle = if last_change == 0 { - translate!("who-idle-unknown").into() + " ?".into() } else { idle_string(last_change, 0) }; From d78314e1636c68eec2d15d2c6ac06eee33583198 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 5 Apr 2026 21:07:56 +0900 Subject: [PATCH 251/264] mkdir: replace libc and unsafe by rustix --- Cargo.lock | 1 + src/uu/mkdir/Cargo.toml | 3 +++ src/uu/mkdir/src/mkdir.rs | 12 +++++------- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 557259533bc..eb641725a0d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3771,6 +3771,7 @@ version = "0.8.0" dependencies = [ "clap", "fluent", + "rustix", "uucore", ] diff --git a/src/uu/mkdir/Cargo.toml b/src/uu/mkdir/Cargo.toml index 6e2e082d044..394bbbeac4d 100644 --- a/src/uu/mkdir/Cargo.toml +++ b/src/uu/mkdir/Cargo.toml @@ -23,6 +23,9 @@ clap = { workspace = true } uucore = { workspace = true, features = ["fs", "mode", "fsxattr"] } fluent = { workspace = true } +[target.'cfg(unix)'.dependencies] +rustix = { workspace = true, features = ["process", "fs"] } + [features] selinux = ["uucore/selinux"] smack = ["uucore/smack"] diff --git a/src/uu/mkdir/src/mkdir.rs b/src/uu/mkdir/src/mkdir.rs index 5db16c1d601..9d632115c54 100644 --- a/src/uu/mkdir/src/mkdir.rs +++ b/src/uu/mkdir/src/mkdir.rs @@ -252,13 +252,13 @@ fn create_dir(path: &Path, is_parent: bool, config: &Config) -> UResult<()> { /// RAII guard to restore umask on drop, ensuring cleanup even on panic. #[cfg(unix)] -struct UmaskGuard(uucore::libc::mode_t); +struct UmaskGuard(rustix::fs::Mode); #[cfg(unix)] impl UmaskGuard { /// Set umask to the given value and return a guard that restores the original on drop. - fn set(new_mask: uucore::libc::mode_t) -> Self { - let old_mask = unsafe { uucore::libc::umask(new_mask) }; + fn set(new_mask: rustix::fs::Mode) -> Self { + let old_mask = rustix::process::umask(new_mask); Self(old_mask) } } @@ -266,9 +266,7 @@ impl UmaskGuard { #[cfg(unix)] impl Drop for UmaskGuard { fn drop(&mut self) { - unsafe { - uucore::libc::umask(self.0); - } + rustix::process::umask(self.0); } } @@ -283,7 +281,7 @@ fn create_dir_with_mode(path: &Path, mode: u32) -> std::io::Result<()> { // Temporarily set umask to 0 so the directory is created with the exact mode. // The guard restores the original umask on drop, even if we panic. - let _guard = UmaskGuard::set(0); + let _guard = UmaskGuard::set(rustix::fs::Mode::empty()); std::fs::DirBuilder::new().mode(mode).create(path) } From ccc51850335087be8014cd9381df9b341af0a0de Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 5 Apr 2026 23:21:04 +0200 Subject: [PATCH 252/264] numfmt: add failing test cases --- tests/by-util/test_numfmt.rs | 95 ++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/tests/by-util/test_numfmt.rs b/tests/by-util/test_numfmt.rs index 4ffc258d799..18a8eab660b 100644 --- a/tests/by-util/test_numfmt.rs +++ b/tests/by-util/test_numfmt.rs @@ -1366,6 +1366,101 @@ fn test_null_byte_input_multiline() { .stdout_is("1000\n3000"); } +// https://github.com/uutils/coreutils/issues/11653 +// GNU rejects `-9923868` as an invalid short option (leading `-9`) and +// requires `--` separator; uutils accepts it as a negative positional number. +#[test] +#[ignore = "GNU compat: see uutils/coreutils#11653"] +fn test_negative_number_without_double_dash_gnu_compat_issue_11653() { + new_ucmd!() + .args(&["--to=iec", "-9923868"]) + .fails_with_code(1) + .stderr_contains("invalid option"); +} + +// https://github.com/uutils/coreutils/issues/11654 +// uutils parses large integers through f64, losing precision past 2^53. +#[test] +#[ignore = "GNU compat: see uutils/coreutils#11654"] +fn test_large_integer_precision_loss_issue_11654() { + new_ucmd!() + .args(&["--from=iec", "9153396227555392131"]) + .succeeds() + .stdout_is("9153396227555392131\n"); +} + +// https://github.com/uutils/coreutils/issues/11655 +// uutils accepts scientific notation (`1e9`, `5e-3`, ...); GNU rejects it +// as "invalid suffix in input". +#[test] +#[ignore = "GNU compat: see uutils/coreutils#11655"] +fn test_scientific_notation_rejected_by_gnu_issue_11655() { + new_ucmd!() + .arg("1e9") + .fails_with_code(2) + .stderr_contains("invalid suffix in input"); +} + +// https://github.com/uutils/coreutils/issues/11662 +// `--to=auto` is accepted at parse time by uutils then rejected at runtime +// with exit code 2; GNU rejects it in option parsing with exit code 1. +#[test] +#[ignore = "GNU compat: see uutils/coreutils#11662"] +fn test_to_auto_rejected_at_parse_time_issue_11662() { + new_ucmd!() + .args(&["--to=auto", "100"]) + .fails_with_code(1) + .stderr_contains("invalid argument 'auto' for '--to'"); +} + +// https://github.com/uutils/coreutils/issues/11663 +// `--from-unit` multiplication with fractional input rounds to an integer; +// GNU preserves the fractional digits. +#[test] +#[ignore = "GNU compat: see uutils/coreutils#11663"] +fn test_from_unit_fractional_precision_issue_11663() { + new_ucmd!() + .args(&["--from=iec", "--from-unit=959", "--", "-615484.454"]) + .succeeds() + .stdout_is("-590249591.386\n"); +} + +// https://github.com/uutils/coreutils/issues/11664 +// Zero-padded `--format` places padding zeros before the sign for negative +// numbers; GNU (and C printf) puts the sign first. +#[test] +#[ignore = "GNU compat: see uutils/coreutils#11664"] +fn test_zero_pad_sign_order_issue_11664() { + new_ucmd!() + .args(&["--from=none", "--format=%018.2f", "--", "-9869647"]) + .succeeds() + .stdout_is("-00000009869647.00\n"); +} + +// https://github.com/uutils/coreutils/issues/11666 +// `--to-unit=N` selects the output prefix based on the unscaled value +// instead of `value / N`. +#[test] +#[ignore = "GNU compat: see uutils/coreutils#11666"] +fn test_to_unit_prefix_selection_issue_11666() { + new_ucmd!() + .args(&["--to=iec-i", "--to-unit=885", "100000"]) + .succeeds() + .stdout_is("113\n"); +} + +// https://github.com/uutils/coreutils/issues/11667 +// `--format='%.0f'` with `--to=` still prints one fractional digit; +// the precision specifier `.0` is ignored. +#[test] +#[ignore = "GNU compat: see uutils/coreutils#11667"] +fn test_format_precision_zero_with_to_scale_issue_11667() { + new_ucmd!() + .args(&["--to=iec", "--format=%.0f", "5183776"]) + .succeeds() + .stdout_is("5M\n"); +} + #[test] fn test_invalid_utf8_input() { // 0xFF is invalid UTF-8 From 9e67e56f279aae62a53bb7afbf253593c3cff0ca Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 5 Apr 2026 22:42:52 +0200 Subject: [PATCH 253/264] date: add ignored tests for GNU compatibility gaps found by fuzz_date --- tests/by-util/test_date.rs | 163 +++++++++++++++++++++++++++++++++++++ 1 file changed, 163 insertions(+) diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index df5c58b2f34..de347a17d7a 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -4,6 +4,7 @@ // file that was distributed with this source code. // // spell-checker: ignore: AEDT AEST EEST NZDT NZST Kolkata Iseconds févr février janv janvier mercredi samedi sommes juin décembre Januar Juni Dezember enero junio diciembre gennaio giugno dicembre junho dezembro lundi dimanche Montag Sonntag Samstag sábado febr MEST KST uueuu ueuu +// spell-checker: ignore: uppercases use std::cmp::Ordering; @@ -1825,6 +1826,168 @@ fn test_date_parenthesis_comment() { } } +#[test] +#[ignore = "https://github.com/uutils/coreutils/issues/11660 — GNU date pads to the requested width when it is narrower than the default (e.g. `%02j` on day-1 -> `01`); uutils strips leading zeros to `1`."] +fn test_date_strftime_narrow_width_on_wide_default() { + // `%j` has a default width of 3. Requesting `%02j` on day 1 should yield `01`. + // uutils currently yields `1`. + new_ucmd!() + .env("LC_ALL", "C") + .env("TZ", "UTC") + .arg("-d") + .arg("2024-01-01") + .arg("+%02j") + .succeeds() + .stdout_is("01\n"); +} + +#[test] +#[ignore = "https://github.com/uutils/parse_datetime/issues/283 — GNU date floors negative fractional epochs (`@-1.5` -> -2); uutils truncates toward zero (-> -1)."] +fn test_date_negative_fractional_epoch_flooring() { + new_ucmd!() + .env("LC_ALL", "C") + .env("TZ", "UTC") + .arg("-d") + .arg("@-1.5") + .arg("+%s") + .succeeds() + .stdout_is("-2\n"); +} + +#[test] +#[ignore = "https://github.com/uutils/parse_datetime/issues/282 — parse_datetime rejects `HH:MM am/pm` forms (e.g. `2024-06-15 12:00 PM`, `2024-06-15 11:30am`). GNU date accepts them."] +fn test_date_input_hhmm_ampm() { + for input in [ + "2024-06-15 12:00 PM", + "2024-06-15 11:30am", + "2024-06-15 3:00 PM", + ] { + new_ucmd!() + .env("LC_ALL", "C") + .env("TZ", "UTC") + .arg("-d") + .arg(input) + .arg("+%H:%M") + .succeeds(); + } +} + +#[test] +#[ignore = "https://github.com/uutils/parse_datetime/issues/281 — GNU date re-zones input with trailing TZ abbreviation (e.g. `2024-01-01 EST`) into the local TZ; uutils keeps the input TZ on output."] +fn test_date_input_trailing_tz_abbrev_rezones() { + // `TZ=UTC+1 date -d '2024-01-01 EST'` should display the instant in UTC+1 + // (GNU: 04:00:00 UTC), not leave it in EST (uutils: 00:00:00 -05). + new_ucmd!() + .env("LC_ALL", "C") + .env("TZ", "UTC+1") + .arg("-d") + .arg("2024-01-01 EST") + .arg("+%H:%M:%S %:z") + .succeeds() + .stdout_is("04:00:00 -01:00\n"); +} + +#[test] +#[ignore = "https://github.com/uutils/coreutils/issues/11659 — GNU date treats the `#` case-swap flag as no-op on `%P` (already-lowercase alt); uutils uppercases it to `PM`."] +fn test_date_strftime_case_flag_on_alt_ampm() { + // `%P` is GNU's lowercase am/pm. `%#P` should stay lowercase in GNU; uutils flips to `PM`. + new_ucmd!() + .env("LC_ALL", "C") + .env("TZ", "UTC") + .arg("-d") + .arg("2024-06-15 13:45:30") + .arg("+%#P") + .succeeds() + .stdout_is("pm\n"); +} + +#[test] +#[ignore = "https://github.com/uutils/coreutils/issues/11658 — GNU date applies flags/widths to `%N` (nanoseconds); uutils ignores/mishandles them."] +fn test_date_strftime_n_width_and_flags() { + // `%_3N` should space-pad nanoseconds to width 3. GNU outputs `0 `; uutils outputs `0`. + new_ucmd!() + .env("LC_ALL", "C") + .env("TZ", "UTC") + .arg("-d") + .arg("@0") + .arg("+%_3N") + .succeeds() + .stdout_is("0 \n"); + // `%-N` (no-padding flag) should still output the full 9-digit default. + // GNU: `000000000`; uutils: `0`. + new_ucmd!() + .env("LC_ALL", "C") + .env("TZ", "UTC") + .arg("-d") + .arg("@0") + .arg("+%-N") + .succeeds() + .stdout_is("000000000\n"); +} + +#[test] +#[ignore = "https://github.com/uutils/coreutils/issues/11657 — GNU date treats composite strftime specifiers (%D, %F, %T, ...) as atomic; flags like `-` should not propagate to sub-fields."] +fn test_date_strftime_flag_on_composite() { + // GNU `%-D` keeps `06/15/24` (flag ignored on composite). + // uutils applies `-` to inner `%m`, producing `6/15/24`. + new_ucmd!() + .env("LC_ALL", "C") + .env("TZ", "UTC") + .arg("-d") + .arg("2024-06-15") + .arg("+%-D") + .succeeds() + .stdout_is("06/15/24\n"); +} + +#[test] +#[ignore = "https://github.com/uutils/coreutils/issues/11656 — GNU date strips the `O` strftime modifier in C locale (e.g. `%Om` -> `%m`); uutils leaks it as literal `%om`."] +fn test_date_strftime_o_modifier() { + // In C locale the `O` modifier is a no-op (alternative numeric symbols). + // GNU renders `%Om` as `06` for June; uutils renders it as the literal `%Om`. + new_ucmd!() + .env("LC_ALL", "C") + .env("TZ", "UTC") + .arg("-d") + .arg("2024-06-15") + .arg("+%Om-%Oy-%Ol") + .succeeds() + .stdout_is("06-24-12\n"); +} + +#[test] +#[ignore = "https://github.com/uutils/parse_datetime/issues/280 — GNU date accepts bare timezone abbreviations (UT, GMT, ...) meaning `now in that TZ`; parse_datetime rejects them."] +fn test_date_bare_timezone_abbreviation() { + // GNU: `date -d ut`, `date -d UT`, `date -d gmt` → current time in UTC. + // uutils: "invalid date" error. + for input in ["ut", "UT", "gmt", "GMT"] { + new_ucmd!() + .env("TZ", "UTC+1") + .arg("-d") + .arg(input) + .arg("+%Z") + .succeeds() + .stdout_is("UTC\n"); + } +} + +#[test] +#[ignore = "https://github.com/uutils/parse_datetime/issues/279 — GNU date silently ignores unrecognized trailing tokens (e.g. `8j`), but parse_datetime rejects them."] +fn test_date_ignores_unrecognized_trailing_tokens() { + // GNU compatibility: trailing unknown word-tokens after a valid number are ignored. + // GNU parses `8j`, `8 j`, etc. as hour 8; our parse_datetime crate errors out. + for input in ["8j", "8 j"] { + new_ucmd!() + .env("TZ", "UTC") + .arg("-u") + .arg("-d") + .arg(input) + .arg("+%H:%M:%S") + .succeeds() + .stdout_only("08:00:00\n"); + } +} + #[test] fn test_date_parenthesis_vs_other_special_chars() { // Ensure parentheses are special but other chars like [, ., ^ are still rejected From a4dcf842eb737528753f79e7d19b4aae1dcc115e Mon Sep 17 00:00:00 2001 From: xtqqczze <45661989+xtqqczze@users.noreply.github.com> Date: Fri, 3 Apr 2026 18:40:03 +0100 Subject: [PATCH 254/264] pre-commit: update cargo-lock-check to suggest changes --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9abeb5ca476..2ca1cdab94c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -47,7 +47,7 @@ repos: - id: cargo-lock-check name: Cargo.lock sync check description: Ensure Cargo.lock and fuzz/Cargo.lock are up-to-date. - entry: bash -c 'for dir in . fuzz; do ( cd "$dir" && cargo fetch --locked --quiet ) || { echo "ERROR - $dir/Cargo.lock is out of date. Run -> cd $dir && cargo update"; exit 1; } done' + entry: bash -c 'for dir in . fuzz; do ( cd "$dir" && cargo fetch --quiet ); done' pass_filenames: false files: 'Cargo\.(toml|lock)$' language: system From e6f06874107525c59966a0957fa1867b5bb05524 Mon Sep 17 00:00:00 2001 From: Ibrahim Burak Yorulmaz Date: Mon, 6 Apr 2026 08:16:40 +0200 Subject: [PATCH 255/264] dd: do not show zero multiplier warning when zero is the multiplicand (#11673) * dd: do not show zero multiplier warning when zero is the multiplicand * tests/dd: test zero multiplier warning when zero is the multiplicand --- src/uu/dd/src/parseargs.rs | 4 ++-- tests/by-util/test_dd.rs | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/uu/dd/src/parseargs.rs b/src/uu/dd/src/parseargs.rs index 42235cabda5..0df5c2b3886 100644 --- a/src/uu/dd/src/parseargs.rs +++ b/src/uu/dd/src/parseargs.rs @@ -524,8 +524,8 @@ pub fn parse_bytes_with_opt_multiplier(s: &str) -> Result { parse_bytes_no_x(s, parts[0]) } else { let mut total: u64 = 1; - for part in parts { - if part == "0" { + for (i, part) in parts.iter().enumerate() { + if *part == "0" && i != parts.len() - 1 { show_zero_multiplier_warning(); } let num = parse_bytes_no_x(s, part)?; diff --git a/tests/by-util/test_dd.rs b/tests/by-util/test_dd.rs index d2dfb7bda2c..e9574bb0549 100644 --- a/tests/by-util/test_dd.rs +++ b/tests/by-util/test_dd.rs @@ -263,6 +263,13 @@ fn test_zero_multiplier_warning() { .succeeds() .no_stdout() .stderr_contains("warning: '0x' is a zero multiplier; use '00x' if that is intended"); + + new_ucmd!() + .args(&[format!("{arg}=0x0x0").as_str(), "status=none"]) + .pipe_in("") + .succeeds() + .no_stdout() + .stderr_is("dd: warning: '0x' is a zero multiplier; use '00x' if that is intended\ndd: warning: '0x' is a zero multiplier; use '00x' if that is intended\n"); } } From aa19343f20bff7c533cb45b7fa201f1c97a42abe Mon Sep 17 00:00:00 2001 From: vishakha1411 Date: Mon, 6 Apr 2026 08:06:10 +0530 Subject: [PATCH 256/264] date: fix %#P case-swap on lowercase am/pm --- src/uu/date/src/format_modifiers.rs | 5 ++++- tests/by-util/test_date.rs | 1 - 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/uu/date/src/format_modifiers.rs b/src/uu/date/src/format_modifiers.rs index 8c18c3cca71..8fffaf1ce8c 100644 --- a/src/uu/date/src/format_modifiers.rs +++ b/src/uu/date/src/format_modifiers.rs @@ -337,7 +337,10 @@ fn apply_modifiers( .all(|c| !c.is_alphabetic() || c.is_uppercase()) { result = result.to_lowercase(); - } else { + } else if !result + .chars() + .all(|c| !c.is_alphabetic() || c.is_lowercase()) + { result = result.to_uppercase(); } } diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index de347a17d7a..919a096d01c 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -1888,7 +1888,6 @@ fn test_date_input_trailing_tz_abbrev_rezones() { } #[test] -#[ignore = "https://github.com/uutils/coreutils/issues/11659 — GNU date treats the `#` case-swap flag as no-op on `%P` (already-lowercase alt); uutils uppercases it to `PM`."] fn test_date_strftime_case_flag_on_alt_ampm() { // `%P` is GNU's lowercase am/pm. `%#P` should stay lowercase in GNU; uutils flips to `PM`. new_ucmd!() From cb18e9e871a06ea5c08066b6cd4919391f5b371b Mon Sep 17 00:00:00 2001 From: vishakha1411 Date: Mon, 6 Apr 2026 01:37:34 +0000 Subject: [PATCH 257/264] date: honor narrow width on wide default strftime fields --- src/uu/date/src/format_modifiers.rs | 4 ++-- tests/by-util/test_date.rs | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/uu/date/src/format_modifiers.rs b/src/uu/date/src/format_modifiers.rs index 8fffaf1ce8c..7cdf32b174c 100644 --- a/src/uu/date/src/format_modifiers.rs +++ b/src/uu/date/src/format_modifiers.rs @@ -359,9 +359,9 @@ fn apply_modifiers( width }; - // Handle width smaller than result: strip default padding to fit + // When the requested width is narrower than the default formatted width, GNU first removes default padding and then reapplies the requested width. if effective_width > 0 && effective_width < result.len() { - return Ok(strip_default_padding(&result)); + result = strip_default_padding(&result); } // Strip default padding when switching pad characters on numeric fields diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index 919a096d01c..3b8bd13f83d 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -1827,7 +1827,6 @@ fn test_date_parenthesis_comment() { } #[test] -#[ignore = "https://github.com/uutils/coreutils/issues/11660 — GNU date pads to the requested width when it is narrower than the default (e.g. `%02j` on day-1 -> `01`); uutils strips leading zeros to `1`."] fn test_date_strftime_narrow_width_on_wide_default() { // `%j` has a default width of 3. Requesting `%02j` on day 1 should yield `01`. // uutils currently yields `1`. From ae7e873ed302082bcc135074777ffc7651c01e35 Mon Sep 17 00:00:00 2001 From: Eshwar Singh Rajaputana <145380153+Eshwar1440@users.noreply.github.com> Date: Mon, 6 Apr 2026 03:58:18 -0500 Subject: [PATCH 258/264] numfmt: preserve fractional digits when --from-unit is used without suffix and fixed String format (#11674) * numfmt: preserve fractional digits when --from-unit is used without suffix and fixed String format * numfmt: simplify redundant if branches caught by clippy * numfmt: fix precision condition to only preserve decimals when input has no unit suffix --- src/uu/numfmt/src/format.rs | 10 ++++++++-- tests/by-util/test_numfmt.rs | 1 - 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/uu/numfmt/src/format.rs b/src/uu/numfmt/src/format.rs index 46f7336e5ee..e1b5101f235 100644 --- a/src/uu/numfmt/src/format.rs +++ b/src/uu/numfmt/src/format.rs @@ -374,11 +374,12 @@ fn transform_from(s: &str, opts: &TransformOptions, options: &NumfmtOptions) -> .map_err(|original| { detailed_error_message(s, opts.from, &options.unit_separator).unwrap_or(original) })?; + let had_no_suffix = suffix.is_none(); let i = i * (opts.from_unit as f64); remove_suffix(i, suffix, opts.from).map(|n| { // GNU numfmt doesn't round values if no --from argument is provided by the user - if opts.from == Unit::None { + if opts.from == Unit::None || had_no_suffix { if n == -0.0 { 0.0 } else { n } } else if n < 0.0 { -n.abs().ceil() @@ -541,7 +542,12 @@ fn format_string( let precision = if let Some(p) = options.format.precision { p - } else if options.transform.from == Unit::None && options.transform.to == Unit::None { + } else if options.transform.to == Unit::None + && !source_without_suffix + .chars() + .last() + .is_some_and(char::is_alphabetic) + { parse_implicit_precision(source_without_suffix) } else { 0 diff --git a/tests/by-util/test_numfmt.rs b/tests/by-util/test_numfmt.rs index 18a8eab660b..ae03c9ffd5e 100644 --- a/tests/by-util/test_numfmt.rs +++ b/tests/by-util/test_numfmt.rs @@ -1417,7 +1417,6 @@ fn test_to_auto_rejected_at_parse_time_issue_11662() { // `--from-unit` multiplication with fractional input rounds to an integer; // GNU preserves the fractional digits. #[test] -#[ignore = "GNU compat: see uutils/coreutils#11663"] fn test_from_unit_fractional_precision_issue_11663() { new_ucmd!() .args(&["--from=iec", "--from-unit=959", "--", "-615484.454"]) From be211277ddd0f6a63bd99ff3544cd6e6f20099e6 Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Mon, 6 Apr 2026 10:50:00 +0200 Subject: [PATCH 259/264] cat: remove some unnecessary to_owned() calls --- src/uu/cat/src/cat.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/uu/cat/src/cat.rs b/src/uu/cat/src/cat.rs index ee75259f58d..a572e56d25a 100644 --- a/src/uu/cat/src/cat.rs +++ b/src/uu/cat/src/cat.rs @@ -229,26 +229,26 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { }; let show_nonprint = [ - options::SHOW_ALL.to_owned(), - options::SHOW_NONPRINTING_ENDS.to_owned(), - options::SHOW_NONPRINTING_TABS.to_owned(), - options::SHOW_NONPRINTING.to_owned(), + options::SHOW_ALL, + options::SHOW_NONPRINTING_ENDS, + options::SHOW_NONPRINTING_TABS, + options::SHOW_NONPRINTING, ] .iter() .any(|v| matches.get_flag(v)); let show_ends = [ - options::SHOW_ENDS.to_owned(), - options::SHOW_ALL.to_owned(), - options::SHOW_NONPRINTING_ENDS.to_owned(), + options::SHOW_ENDS, + options::SHOW_ALL, + options::SHOW_NONPRINTING_ENDS, ] .iter() .any(|v| matches.get_flag(v)); let show_tabs = [ - options::SHOW_ALL.to_owned(), - options::SHOW_TABS.to_owned(), - options::SHOW_NONPRINTING_TABS.to_owned(), + options::SHOW_ALL, + options::SHOW_TABS, + options::SHOW_NONPRINTING_TABS, ] .iter() .any(|v| matches.get_flag(v)); From 23af1ad8721e5dfee2287cb5fcb7fc6d0fe3612b Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Mon, 6 Apr 2026 20:35:47 +0900 Subject: [PATCH 260/264] head: split head.rs --- src/uu/head/src/cli.rs | 84 +++++++++++++++++++++++++++++++++++++++++ src/uu/head/src/head.rs | 81 +++------------------------------------ 2 files changed, 89 insertions(+), 76 deletions(-) create mode 100644 src/uu/head/src/cli.rs diff --git a/src/uu/head/src/cli.rs b/src/uu/head/src/cli.rs new file mode 100644 index 00000000000..823d3869116 --- /dev/null +++ b/src/uu/head/src/cli.rs @@ -0,0 +1,84 @@ +// This file is part of the uutils coreutils package. +// +// 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::ffi::OsString; +use uucore::format_usage; +use uucore::translate; + +pub mod options { + pub const BYTES: &str = "BYTES"; + pub const LINES: &str = "LINES"; + pub const QUIET: &str = "QUIET"; + pub const VERBOSE: &str = "VERBOSE"; + pub const ZERO: &str = "ZERO"; + pub const FILES: &str = "FILE"; + pub const PRESUME_INPUT_PIPE: &str = "-PRESUME-INPUT-PIPE"; +} + +pub fn uu_app() -> Command { + Command::new("head") + .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template("head")) + .about(translate!("head-about")) + .override_usage(format_usage(&translate!("head-usage"))) + .infer_long_args(true) + .arg( + Arg::new(options::BYTES) + .short('c') + .long("bytes") + .value_name("[-]NUM") + .help(translate!("head-help-bytes")) + .overrides_with_all([options::BYTES, options::LINES]) + .allow_hyphen_values(true), + ) + .arg( + Arg::new(options::LINES) + .short('n') + .long("lines") + .value_name("[-]NUM") + .help(translate!("head-help-lines")) + .overrides_with_all([options::LINES, options::BYTES]) + .allow_hyphen_values(true), + ) + .arg( + Arg::new(options::QUIET) + .short('q') + .long("quiet") + .visible_alias("silent") + .help(translate!("head-help-quiet")) + .overrides_with_all([options::VERBOSE, options::QUIET]) + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new(options::VERBOSE) + .short('v') + .long("verbose") + .help(translate!("head-help-verbose")) + .overrides_with_all([options::QUIET, options::VERBOSE]) + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new(options::PRESUME_INPUT_PIPE) + .long("presume-input-pipe") + .alias("-presume-input-pipe") + .hide(true) + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new(options::ZERO) + .short('z') + .long("zero-terminated") + .help(translate!("head-help-zero-terminated")) + .overrides_with(options::ZERO) + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new(options::FILES) + .action(ArgAction::Append) + .value_parser(clap::value_parser!(OsString)) + .value_hint(clap::ValueHint::FilePath), + ) +} diff --git a/src/uu/head/src/head.rs b/src/uu/head/src/head.rs index 43ebc47aa6b..1fbb21dcb4c 100644 --- a/src/uu/head/src/head.rs +++ b/src/uu/head/src/head.rs @@ -5,7 +5,7 @@ // spell-checker:ignore (vars) seekable memrchr -use clap::{Arg, ArgAction, ArgMatches, Command}; +use clap::ArgMatches; use memchr::memrchr_iter; use std::ffi::OsString; use std::fs::File; @@ -18,20 +18,14 @@ use thiserror::Error; use uucore::display::{Quotable, print_verbatim}; use uucore::error::{FromIo, UError, UResult, USimpleError}; use uucore::line_ending::LineEnding; +use uucore::show; use uucore::translate; -use uucore::{format_usage, show}; const BUF_SIZE: usize = 65536; -mod options { - pub const BYTES: &str = "BYTES"; - pub const LINES: &str = "LINES"; - pub const QUIET: &str = "QUIET"; - pub const VERBOSE: &str = "VERBOSE"; - pub const ZERO: &str = "ZERO"; - pub const FILES: &str = "FILE"; - pub const PRESUME_INPUT_PIPE: &str = "-PRESUME-INPUT-PIPE"; -} +mod cli; +use crate::cli::options; +pub use crate::cli::uu_app; mod parse; mod take; @@ -66,71 +60,6 @@ impl UError for HeadError { type HeadResult = Result; -pub fn uu_app() -> Command { - Command::new("head") - .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template("head")) - .about(translate!("head-about")) - .override_usage(format_usage(&translate!("head-usage"))) - .infer_long_args(true) - .arg( - Arg::new(options::BYTES) - .short('c') - .long("bytes") - .value_name("[-]NUM") - .help(translate!("head-help-bytes")) - .overrides_with_all([options::BYTES, options::LINES]) - .allow_hyphen_values(true), - ) - .arg( - Arg::new(options::LINES) - .short('n') - .long("lines") - .value_name("[-]NUM") - .help(translate!("head-help-lines")) - .overrides_with_all([options::LINES, options::BYTES]) - .allow_hyphen_values(true), - ) - .arg( - Arg::new(options::QUIET) - .short('q') - .long("quiet") - .visible_alias("silent") - .help(translate!("head-help-quiet")) - .overrides_with_all([options::VERBOSE, options::QUIET]) - .action(ArgAction::SetTrue), - ) - .arg( - Arg::new(options::VERBOSE) - .short('v') - .long("verbose") - .help(translate!("head-help-verbose")) - .overrides_with_all([options::QUIET, options::VERBOSE]) - .action(ArgAction::SetTrue), - ) - .arg( - Arg::new(options::PRESUME_INPUT_PIPE) - .long("presume-input-pipe") - .alias("-presume-input-pipe") - .hide(true) - .action(ArgAction::SetTrue), - ) - .arg( - Arg::new(options::ZERO) - .short('z') - .long("zero-terminated") - .help(translate!("head-help-zero-terminated")) - .overrides_with(options::ZERO) - .action(ArgAction::SetTrue), - ) - .arg( - Arg::new(options::FILES) - .action(ArgAction::Append) - .value_parser(clap::value_parser!(OsString)) - .value_hint(clap::ValueHint::FilePath), - ) -} - #[derive(Debug, PartialEq)] enum Mode { FirstLines(u64), From 6b5f2e67afb12bf812076a3061ca59910d908bdc Mon Sep 17 00:00:00 2001 From: MegaBrutal Date: Thu, 12 Mar 2026 23:45:00 +0100 Subject: [PATCH 261/264] Tests for Hungarian locale --- tests/by-util/test_date.rs | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index 3b8bd13f83d..489f15158f8 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -1622,6 +1622,32 @@ fn test_date_locale_en_us_vs_c_difference() { } } +#[test] +#[cfg(unix)] +fn test_date_locale_hu_hungarian() { + // Regression test for uutils/coreutils#11240: the GNU modifier fast-path + // ("%-e") used to run before ICU localization, so "%b"/"%A" came out in + // English even under hu_HU.UTF-8. Pin an explicit format string so the + // assertion is deterministic across glibc versions (the default D_T_FMT + // for hu_HU differs between distros). + if !is_locale_available("hu_HU.UTF-8") { + return; + } + + let result = new_ucmd!() + .env("LC_ALL", "hu_HU.UTF-8") + .env("TZ", "UTC") + .arg("-d") + .arg("2025-12-14T13:00:00") + .arg("+%Y. %b %-e., %A, %H:%M:%S %Z") + .succeeds(); + + assert_eq!( + result.stdout_str(), + "2025. dec 14., vasárnap, 13:00:00 UTC\n" + ); +} + #[test] #[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))] fn test_date_locale_fr_french() { @@ -2273,6 +2299,7 @@ fn test_locale_month_names() { ("es_ES.UTF-8", "enero", "junio", "diciembre"), ("it_IT.UTF-8", "gennaio", "giugno", "dicembre"), ("pt_BR.UTF-8", "janeiro", "junho", "dezembro"), + ("hu_HU.UTF-8", "január", "június", "december"), ("ja_JP.UTF-8", "1月", "6月", "12月"), ("zh_CN.UTF-8", "一月", "六月", "十二月"), ] { From 3ecbb52b0ca68f0d427e4303f8e728fefe08fa65 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 5 Apr 2026 22:14:17 +0200 Subject: [PATCH 262/264] date: apply locale-aware localization before GNU modifier processing Formats like "%-e" previously bypassed ICU localization of "%b"/"%A" because the modifier fast-path ran before localize_format_string(). Localize first, then run the modifier pass on the already-localized format. Fixes uutils/coreutils#11240. --- src/uu/date/src/date.rs | 38 +++++++++++++++++--------------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/src/uu/date/src/date.rs b/src/uu/date/src/date.rs index 89ee23fd31e..ab7cbf680c6 100644 --- a/src/uu/date/src/date.rs +++ b/src/uu/date/src/date.rs @@ -709,31 +709,27 @@ fn format_date_with_locale_aware_months( #[cfg(feature = "i18n-datetime")] skip_localization: bool, #[cfg(not(feature = "i18n-datetime"))] _skip_localization: bool, ) -> Result { - // First check if format string has GNU modifiers (width/flags) and format if present - // This optimization combines detection and formatting in a single pass - if let Some(result) = - format_modifiers::format_with_modifiers_if_present(date, format_string, config) - { + // Apply locale-aware name substitution (month/day names) before modifier + // processing, so that formats like "%-e" don't bypass localization of "%b"/"%A". + // The owned String is kept in `localized` so `fmt` can borrow from it for the + // rest of the function without a dangling reference. + #[cfg(feature = "i18n-datetime")] + let localized: Option = (!skip_localization && should_use_icu_locale()) + .then(|| localize_format_string(format_string, date.date())); + #[cfg(feature = "i18n-datetime")] + let fmt: &str = localized.as_deref().unwrap_or(format_string); + #[cfg(not(feature = "i18n-datetime"))] + let fmt = format_string; + + // Check if format string has GNU modifiers (width/flags) and format if present + if let Some(result) = format_modifiers::format_with_modifiers_if_present(date, fmt, config) { return result.map_err(|e| e.to_string()); } let broken_down = BrokenDownTime::from(date); - - // When the i18n-datetime feature is enabled (default), use ICU locale-aware - // formatting if the locale requires it. Without the feature (e.g. wasi/wasm - // builds that use --no-default-features), skip localization entirely and - // format with the raw strftime string. - #[cfg(feature = "i18n-datetime")] - let result = if !should_use_icu_locale() || skip_localization { - broken_down.to_string_with_config(config, format_string) - } else { - let fmt = localize_format_string(format_string, date.date()); - broken_down.to_string_with_config(config, &fmt) - }; - #[cfg(not(feature = "i18n-datetime"))] - let result = broken_down.to_string_with_config(config, format_string); - - result.map_err(|e| e.to_string()) + broken_down + .to_string_with_config(config, fmt) + .map_err(|e| e.to_string()) } /// Return the appropriate format string for the given settings. From 2ef0aea80304f369e84f6e0713726e1158453ac4 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 5 Apr 2026 23:07:49 +0200 Subject: [PATCH 263/264] ci: install hu_HU.UTF-8 (and peers) for l10n cargo test job --- .github/workflows/l10n.yml | 12 ++++++++++++ tests/by-util/test_date.rs | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/l10n.yml b/.github/workflows/l10n.yml index 98504f99778..11b2c52e748 100644 --- a/.github/workflows/l10n.yml +++ b/.github/workflows/l10n.yml @@ -56,6 +56,18 @@ jobs: ubuntu-*) # selinux headers needed for testing sudo apt-get -y update ; sudo apt-get -y install libselinux1-dev + # Locales required by tests/by-util/test_date.rs and + # tests/by-util/test_dd.rs (fr_FR is the ISO-8859-1 variant + # used by test_iso8859_1_case_conversion). + sudo locale-gen --keep-existing fr_FR + sudo locale-gen --keep-existing fr_FR.UTF-8 + sudo locale-gen --keep-existing es_ES.UTF-8 + sudo locale-gen --keep-existing en_US.UTF-8 + sudo locale-gen --keep-existing fa_IR.UTF-8 # Iran + sudo locale-gen --keep-existing am_ET.UTF-8 # Ethiopia + sudo locale-gen --keep-existing th_TH.UTF-8 # Thailand + sudo locale-gen --keep-existing hu_HU.UTF-8 # Hungary + sudo update-locale ;; macos-*) # needed for testing diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index 489f15158f8..7096e2040ee 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -3,7 +3,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. // -// spell-checker: ignore: AEDT AEST EEST NZDT NZST Kolkata Iseconds févr février janv janvier mercredi samedi sommes juin décembre Januar Juni Dezember enero junio diciembre gennaio giugno dicembre junho dezembro lundi dimanche Montag Sonntag Samstag sábado febr MEST KST uueuu ueuu +// spell-checker: ignore: AEDT AEST EEST NZDT NZST Kolkata Iseconds févr février janv janvier mercredi samedi sommes juin décembre Januar Juni Dezember enero junio diciembre gennaio giugno dicembre junho dezembro lundi dimanche Montag Sonntag Samstag sábado febr MEST KST uueuu ueuu vasárnap június január distros // spell-checker: ignore: uppercases use std::cmp::Ordering; From 1bfbb9bbd070ea1e015a30c4882f749d46b93871 Mon Sep 17 00:00:00 2001 From: Kevin Burke Date: Thu, 19 Mar 2026 22:32:55 -0700 Subject: [PATCH 264/264] rm: use "traversal failed" message when readdir fails during recursive removal Co-Authored-By: Claude Opus 4.6 --- tests/by-util/test_rm.rs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/by-util/test_rm.rs b/tests/by-util/test_rm.rs index c2b77ee6d3d..efa4ede0bd7 100644 --- a/tests/by-util/test_rm.rs +++ b/tests/by-util/test_rm.rs @@ -7,6 +7,8 @@ use std::process::Stdio; +#[cfg(unix)] +use rlimit::Resource; use uutests::{at_and_ucmd, new_ucmd, util::TestScenario, util_name}; #[test] @@ -1377,3 +1379,29 @@ fn test_preserve_root_literal_root() { .stderr_contains("it is dangerous to operate recursively on '/'") .stderr_contains("use --no-preserve-root to override this failsafe"); } + +/// Test that "traversal failed" message is shown when readdir fails during +/// recursive removal (e.g., due to file descriptor exhaustion). +#[cfg(unix)] +#[test] +fn test_traversal_failed_on_readdir_error() { + let (at, mut ucmd) = at_and_ucmd!(); + at.mkdir_all("a/b"); + at.touch("a/b/file"); + + // Use a very low file descriptor limit so that dup() inside readdir + // fails with EMFILE ("Too many open files"), triggering the + // "traversal failed" error path. + let result = ucmd + .args(&["-rf", "a"]) + .limit(Resource::NOFILE, 5, 5) + .fails(); + result.stderr_contains("traversal failed"); + // musl libc uses "No file descriptors available" instead of glibc's + // "Too many open files" for EMFILE. + let stderr = result.stderr_str(); + assert!( + stderr.contains("Too many open files") || stderr.contains("No file descriptors available"), + "{stderr:?} does not contain expected EMFILE message" + ); +}