From 4d0c0b30773f20b0969eb50ff5097eea811254b2 Mon Sep 17 00:00:00 2001 From: Stephen Marz Date: Fri, 2 Jan 2026 11:24:32 -0500 Subject: [PATCH 001/112] install: add error messages for failed-to-remove and cannot-stat. --- src/uu/install/locales/en-US.ftl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/uu/install/locales/en-US.ftl b/src/uu/install/locales/en-US.ftl index 0261f7320a2..9020db818da 100644 --- a/src/uu/install/locales/en-US.ftl +++ b/src/uu/install/locales/en-US.ftl @@ -48,7 +48,8 @@ install-error-mutually-exclusive-compare-preserve = Options --compare and --pres install-error-mutually-exclusive-compare-strip = Options --compare and --strip are mutually exclusive install-error-missing-file-operand = missing file operand install-error-missing-destination-operand = missing destination file operand after { $path } -install-error-failed-to-remove = Failed to remove existing file { $path }. Error: { $error } +install-error-failed-to-remove = failed to remove existing file { $path }: { $error } +install-error-cannot-stat = cannot stat { $path }: { $error } # Warning messages install-warning-compare-ignored = the --compare (-C) option is ignored when you specify a mode with non-permission bits From 8430b54c8c692161b52d2aec300af01bcaf856ab Mon Sep 17 00:00:00 2001 From: Stephen Marz Date: Fri, 2 Jan 2026 11:24:40 -0500 Subject: [PATCH 002/112] install: add error messages for failed-to-remove and cannot-stat in French. --- src/uu/install/locales/fr-FR.ftl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/uu/install/locales/fr-FR.ftl b/src/uu/install/locales/fr-FR.ftl index 208712c2187..8296698dd46 100644 --- a/src/uu/install/locales/fr-FR.ftl +++ b/src/uu/install/locales/fr-FR.ftl @@ -48,7 +48,8 @@ install-error-mutually-exclusive-compare-preserve = Les options --compare et --p install-error-mutually-exclusive-compare-strip = Les options --compare et --strip sont mutuellement exclusives install-error-missing-file-operand = opérande de fichier manquant install-error-missing-destination-operand = opérande de fichier de destination manquant après { $path } -install-error-failed-to-remove = Échec de la suppression du fichier existant { $path }. Erreur : { $error } +install-error-failed-to-remove = Échec de la suppression du fichier existant { $path }: { $error } +install-error-cannot-stat = impossible d'obtenir des informations sur le fichier. { $path }: { $error } # Messages d'avertissement install-warning-compare-ignored = l'option --compare (-C) est ignorée quand un mode est indiqué avec des bits non liés à des droits From dc70cf7102ef023e65dd2d2aa293a094840c7fad Mon Sep 17 00:00:00 2001 From: Stephen Marz Date: Fri, 2 Jan 2026 11:26:10 -0500 Subject: [PATCH 003/112] install: add error messages to align with GNU for failed-to-remove and cannot-stat. --- src/uu/install/src/install.rs | 40 ++++++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/src/uu/install/src/install.rs b/src/uu/install/src/install.rs index 8e43d1fd2ea..3ab4e938ae6 100644 --- a/src/uu/install/src/install.rs +++ b/src/uu/install/src/install.rs @@ -118,6 +118,12 @@ enum InstallError { #[error("{}", translate!("install-error-extra-operand", "operand" => .0.quote(), "usage" => .1.clone()))] ExtraOperand(OsString, String), + #[error("{}", translate!("install-error-failed-to-remove", "path" => .0.quote(), "error" => format!("{}", .1)))] + FailedToRemove(PathBuf, String), + + #[error("{}", translate!("install-error-cannot-stat", "path" => .0.quote(), "error" => format!("{}", .1)))] + CannotStat(PathBuf, String), + #[cfg(feature = "selinux")] #[error("{}", .0)] SelinuxContextFailed(String), @@ -675,7 +681,19 @@ fn standard(mut paths: Vec, b: &Behavior) -> UResult<()> { return copy_files_into_dir(sources, &target, b); } - if target.is_file() || is_new_file_path(&target) { + // target.is_file does not detect special files like character/block devices + // So, in a unix environment, we need to check the file type from metadata and + // not just trust is_file() + #[cfg(unix)] + let is_fl = { + let fl_type = std::fs::Metadata::from(std::fs::metadata(source)?).file_type(); + fl_type.is_file() || fl_type.is_char_device() || fl_type.is_block_device() || fl_type.is_fifo() + }; + + #[cfg(not(unix))] + let is_fl = target.is_file(); + + if is_fl || is_new_file_path(&target) { copy(source, &target, b) } else { Err(InstallError::InvalidTarget(target).into()) @@ -831,6 +849,18 @@ fn copy_file(from: &Path, to: &Path) -> UResult<()> { } } + // If we don't include this check, then the `remove_file` below will fail + // and it will give an incorrect error message. However, if we check to + // see if the file exists, and it can't even be checked, this means we + // don't have permission to access the file, so we should return an error. + let to_stat = to.try_exists(); + if to_stat.is_err() { + return Err(InstallError::CannotStat( + to.to_path_buf(), + to_stat.err().unwrap().to_string(), + ).into()); + } + if to.is_dir() && !from.is_dir() { return Err(InstallError::OverrideDirectoryFailed( to.to_path_buf().clone(), @@ -842,10 +872,10 @@ fn copy_file(from: &Path, to: &Path) -> UResult<()> { // so lets just remove all existing files at destination before copy. if let Err(e) = fs::remove_file(to) { if e.kind() != std::io::ErrorKind::NotFound { - show_error!( - "{}", - translate!("install-error-failed-to-remove", "path" => to.quote(), "error" => format!("{e:?}")) - ); + // If we get here, then remove_file failed for some + // reason other than the file not existing. This means + // this should be a fatal error, not a warning. + return Err(InstallError::FailedToRemove(to.to_path_buf(), e.to_string()).into()); } } From 70956686c4cb0d4b2fba3b7a0fcc5e8d29712638 Mon Sep 17 00:00:00 2001 From: Stephen Marz Date: Sat, 3 Jan 2026 14:12:21 -0500 Subject: [PATCH 004/112] install: fix offending code that failed tests. Several cross-compatiblity checks added and fixed. --- src/uu/install/src/install.rs | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/uu/install/src/install.rs b/src/uu/install/src/install.rs index 3ab4e938ae6..ae036828f47 100644 --- a/src/uu/install/src/install.rs +++ b/src/uu/install/src/install.rs @@ -118,10 +118,10 @@ enum InstallError { #[error("{}", translate!("install-error-extra-operand", "operand" => .0.quote(), "usage" => .1.clone()))] ExtraOperand(OsString, String), - #[error("{}", translate!("install-error-failed-to-remove", "path" => .0.quote(), "error" => format!("{}", .1)))] + #[error("{}", translate!("install-error-failed-to-remove", "path" => .0.quote(), "error" => .1.clone()))] FailedToRemove(PathBuf, String), - #[error("{}", translate!("install-error-cannot-stat", "path" => .0.quote(), "error" => format!("{}", .1)))] + #[error("{}", translate!("install-error-cannot-stat", "path" => .0.quote(), "error" => .1.clone()))] CannotStat(PathBuf, String), #[cfg(feature = "selinux")] @@ -683,17 +683,17 @@ fn standard(mut paths: Vec, b: &Behavior) -> UResult<()> { // target.is_file does not detect special files like character/block devices // So, in a unix environment, we need to check the file type from metadata and - // not just trust is_file() + // not just trust is_file(). #[cfg(unix)] - let is_fl = { - let fl_type = std::fs::Metadata::from(std::fs::metadata(source)?).file_type(); - fl_type.is_file() || fl_type.is_char_device() || fl_type.is_block_device() || fl_type.is_fifo() + let is_file = match metadata(&target) { + Ok(meta) => !meta.file_type().is_dir(), + Err(_) => false, }; #[cfg(not(unix))] - let is_fl = target.is_file(); + let is_file = target.is_file(); - if is_fl || is_new_file_path(&target) { + if is_file || is_new_file_path(&target) { copy(source, &target, b) } else { Err(InstallError::InvalidTarget(target).into()) @@ -853,11 +853,10 @@ fn copy_file(from: &Path, to: &Path) -> UResult<()> { // and it will give an incorrect error message. However, if we check to // see if the file exists, and it can't even be checked, this means we // don't have permission to access the file, so we should return an error. - let to_stat = to.try_exists(); - if to_stat.is_err() { + if let Err(to_stat) = to.try_exists() { return Err(InstallError::CannotStat( to.to_path_buf(), - to_stat.err().unwrap().to_string(), + to_stat.to_string(), ).into()); } From 46c931213ab6870324c7a8cdbf9c5119f19c522b Mon Sep 17 00:00:00 2001 From: Stephen Marz Date: Sun, 4 Jan 2026 08:51:15 -0500 Subject: [PATCH 005/112] install: run 'cargo fmt' on new error message code. --- src/uu/install/src/install.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/uu/install/src/install.rs b/src/uu/install/src/install.rs index ae036828f47..2cea23b1e05 100644 --- a/src/uu/install/src/install.rs +++ b/src/uu/install/src/install.rs @@ -854,10 +854,7 @@ fn copy_file(from: &Path, to: &Path) -> UResult<()> { // see if the file exists, and it can't even be checked, this means we // don't have permission to access the file, so we should return an error. if let Err(to_stat) = to.try_exists() { - return Err(InstallError::CannotStat( - to.to_path_buf(), - to_stat.to_string(), - ).into()); + return Err(InstallError::CannotStat(to.to_path_buf(), to_stat.to_string()).into()); } if to.is_dir() && !from.is_dir() { From 4a5e03e6241f394c19d627a51350f3b267bd6b1c Mon Sep 17 00:00:00 2001 From: kdtie <59813592+nutthawit@users.noreply.github.com> Date: Tue, 30 Dec 2025 12:54:43 +0700 Subject: [PATCH 006/112] DEVELOPMENT.md: mention nightly requirement for code coverage (#9919) --- DEVELOPMENT.md | 1 + 1 file changed, 1 insertion(+) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 4f885e085dd..35291369c12 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -262,6 +262,7 @@ To generate [gcov-based](https://github.com/mozilla/grcov#example-how-to-generat export CARGO_INCREMENTAL=0 export RUSTFLAGS="-Cinstrument-coverage -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort" export RUSTDOCFLAGS="-Cpanic=abort" +export RUSTUP_TOOLCHAIN="nightly" cargo build # e.g., --features feat_os_unix cargo test # e.g., --features feat_os_unix test_pathchk grcov . -s . --binary-path ./target/debug/ -t html --branch --ignore-not-existing --ignore build.rs --excl-br-line "^\s*((debug_)?assert(_eq|_ne)?\#\[derive\()" -o ./target/debug/coverage/ From 6a2cfca4e228c5ab5a66fa3eae21b481501b1bf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=E1=BA=A3=20th=E1=BA=BF=20gi=E1=BB=9Bi=20l=C3=A0=20Rust?= <90588855+naoNao89@users.noreply.github.com> Date: Wed, 31 Dec 2025 00:37:12 +0700 Subject: [PATCH 007/112] feat(dd): add first benchmark suite for performance validation (#9136) * feat(dd): add comprehensive benchmark suite for O_DIRECT optimization - Create dd's first benchmark suite using divan framework - Benchmark various block sizes (4K, 8K, 64K, 1M) to measure performance - Test different dd scenarios: default, partial copy, skip, seek operations - Measure impact of separate input/output block sizes - All benchmarks use status=none to avoid output noise - Benchmarks verify the O_DIRECT buffer alignment optimization - Follows existing uutils benchmark patterns and conventions * bench(dd): increase dataset sizes for consistent timing Increase benchmark dataset sizes to achieve consistent 100-300ms timing: - dd_copy_default: 16 -> 32 MB - dd_copy_4k_blocks: 16 -> 24 MB - dd_copy_64k_blocks: 16 -> 64 MB - dd_copy_1m_blocks: 16 -> 128 MB - dd_copy_separate_blocks: 16 -> 48 MB - dd_copy_partial: 16 -> 32 MB - dd_copy_with_skip: 16 -> 48 MB - dd_copy_with_seek: 16 -> 48 MB - dd_copy_8k_blocks: 16 -> 32 MB This ensures stable, repeatable benchmark measurements across different systems. --------- Co-authored-by: Sylvestre Ledru --- .github/workflows/benchmarks.yml | 1 + Cargo.lock | 2 + src/uu/dd/Cargo.toml | 9 ++ src/uu/dd/benches/dd_bench.rs | 266 +++++++++++++++++++++++++++++++ 4 files changed, 278 insertions(+) create mode 100644 src/uu/dd/benches/dd_bench.rs diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 205f6c1a24e..1ffce535dae 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -27,6 +27,7 @@ jobs: - { package: uu_cksum } - { package: uu_cp } - { package: uu_cut } + - { package: uu_dd } - { package: uu_du } - { package: uu_expand } - { package: uu_fold } diff --git a/Cargo.lock b/Cargo.lock index 277321cd990..909c0351005 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3201,11 +3201,13 @@ name = "uu_dd" version = "0.5.0" dependencies = [ "clap", + "codspeed-divan-compat", "fluent", "gcd", "libc", "nix", "signal-hook", + "tempfile", "thiserror 2.0.17", "uucore", ] diff --git a/src/uu/dd/Cargo.toml b/src/uu/dd/Cargo.toml index d1ac79fb52e..6dbc6c2ffa0 100644 --- a/src/uu/dd/Cargo.toml +++ b/src/uu/dd/Cargo.toml @@ -37,3 +37,12 @@ nix = { workspace = true, features = ["fs"] } [[bin]] name = "dd" path = "src/main.rs" + +[dev-dependencies] +divan = { workspace = true } +tempfile = { workspace = true } +uucore = { workspace = true, features = ["benchmark"] } + +[[bench]] +name = "dd_bench" +harness = false diff --git a/src/uu/dd/benches/dd_bench.rs b/src/uu/dd/benches/dd_bench.rs new file mode 100644 index 00000000000..0a86f5de18b --- /dev/null +++ b/src/uu/dd/benches/dd_bench.rs @@ -0,0 +1,266 @@ +// 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 divan::{Bencher, black_box}; +use std::fs::{self, File}; +use std::io::Write; +use std::path::Path; +use tempfile::TempDir; +use uu_dd::uumain; +use uucore::benchmark::run_util_function; + +fn create_test_file(path: &Path, size_mb: usize) { + let buffer = vec![b'x'; size_mb * 1024 * 1024]; + let mut file = File::create(path).unwrap(); + file.write_all(&buffer).unwrap(); + file.sync_all().unwrap(); +} + +fn remove_file(path: &Path) { + if path.exists() { + fs::remove_file(path).unwrap(); + } +} + +/// Benchmark basic dd copy with default settings +#[divan::bench(args = [32])] +fn dd_copy_default(bencher: Bencher, size_mb: usize) { + let temp_dir = TempDir::new().unwrap(); + let input = temp_dir.path().join("input.bin"); + let output = temp_dir.path().join("output.bin"); + + create_test_file(&input, size_mb); + + let input_str = input.to_str().unwrap(); + let output_str = output.to_str().unwrap(); + + bencher.bench(|| { + remove_file(&output); + black_box(run_util_function( + uumain, + &[ + &format!("if={input_str}"), + &format!("of={output_str}"), + "status=none", + ], + )); + }); +} + +/// Benchmark dd copy with 4KB block size (common page size) +#[divan::bench(args = [24])] +fn dd_copy_4k_blocks(bencher: Bencher, size_mb: usize) { + let temp_dir = TempDir::new().unwrap(); + let input = temp_dir.path().join("input.bin"); + let output = temp_dir.path().join("output.bin"); + + create_test_file(&input, size_mb); + + let input_str = input.to_str().unwrap(); + let output_str = output.to_str().unwrap(); + + bencher.bench(|| { + remove_file(&output); + black_box(run_util_function( + uumain, + &[ + &format!("if={input_str}"), + &format!("of={output_str}"), + "bs=4K", + "status=none", + ], + )); + }); +} + +/// Benchmark dd copy with 64KB block size +#[divan::bench(args = [64])] +fn dd_copy_64k_blocks(bencher: Bencher, size_mb: usize) { + let temp_dir = TempDir::new().unwrap(); + let input = temp_dir.path().join("input.bin"); + let output = temp_dir.path().join("output.bin"); + + create_test_file(&input, size_mb); + + let input_str = input.to_str().unwrap(); + let output_str = output.to_str().unwrap(); + + bencher.bench(|| { + remove_file(&output); + black_box(run_util_function( + uumain, + &[ + &format!("if={input_str}"), + &format!("of={output_str}"), + "bs=64K", + "status=none", + ], + )); + }); +} + +/// Benchmark dd copy with 1MB block size +#[divan::bench(args = [128])] +fn dd_copy_1m_blocks(bencher: Bencher, size_mb: usize) { + let temp_dir = TempDir::new().unwrap(); + let input = temp_dir.path().join("input.bin"); + let output = temp_dir.path().join("output.bin"); + + create_test_file(&input, size_mb); + + let input_str = input.to_str().unwrap(); + let output_str = output.to_str().unwrap(); + + bencher.bench(|| { + remove_file(&output); + black_box(run_util_function( + uumain, + &[ + &format!("if={input_str}"), + &format!("of={output_str}"), + "bs=1M", + "status=none", + ], + )); + }); +} + +/// Benchmark dd copy with separate input and output block sizes +#[divan::bench(args = [48])] +fn dd_copy_separate_blocks(bencher: Bencher, size_mb: usize) { + let temp_dir = TempDir::new().unwrap(); + let input = temp_dir.path().join("input.bin"); + let output = temp_dir.path().join("output.bin"); + + create_test_file(&input, size_mb); + + let input_str = input.to_str().unwrap(); + let output_str = output.to_str().unwrap(); + + bencher.bench(|| { + remove_file(&output); + black_box(run_util_function( + uumain, + &[ + &format!("if={input_str}"), + &format!("of={output_str}"), + "ibs=8K", + "obs=16K", + "status=none", + ], + )); + }); +} + +/// Benchmark dd with count limit (partial copy) +#[divan::bench(args = [32])] +fn dd_copy_partial(bencher: Bencher, size_mb: usize) { + let temp_dir = TempDir::new().unwrap(); + let input = temp_dir.path().join("input.bin"); + let output = temp_dir.path().join("output.bin"); + + create_test_file(&input, size_mb); + + let input_str = input.to_str().unwrap(); + let output_str = output.to_str().unwrap(); + + bencher.bench(|| { + remove_file(&output); + black_box(run_util_function( + uumain, + &[ + &format!("if={input_str}"), + &format!("of={output_str}"), + "bs=4K", + "count=1024", + "status=none", + ], + )); + }); +} + +/// Benchmark dd with skip (seeking in input) +#[divan::bench(args = [48])] +fn dd_copy_with_skip(bencher: Bencher, size_mb: usize) { + let temp_dir = TempDir::new().unwrap(); + let input = temp_dir.path().join("input.bin"); + let output = temp_dir.path().join("output.bin"); + + create_test_file(&input, size_mb); + + let input_str = input.to_str().unwrap(); + let output_str = output.to_str().unwrap(); + + bencher.bench(|| { + remove_file(&output); + black_box(run_util_function( + uumain, + &[ + &format!("if={input_str}"), + &format!("of={output_str}"), + "bs=4K", + "skip=256", + "status=none", + ], + )); + }); +} + +/// Benchmark dd with seek (seeking in output) +#[divan::bench(args = [48])] +fn dd_copy_with_seek(bencher: Bencher, size_mb: usize) { + let temp_dir = TempDir::new().unwrap(); + let input = temp_dir.path().join("input.bin"); + let output = temp_dir.path().join("output.bin"); + + create_test_file(&input, size_mb); + + let input_str = input.to_str().unwrap(); + let output_str = output.to_str().unwrap(); + + bencher.bench(|| { + remove_file(&output); + black_box(run_util_function( + uumain, + &[ + &format!("if={input_str}"), + &format!("of={output_str}"), + "bs=4K", + "seek=256", + "status=none", + ], + )); + }); +} + +/// Benchmark dd with different block sizes for comparison +#[divan::bench(args = [32])] +fn dd_copy_8k_blocks(bencher: Bencher, size_mb: usize) { + let temp_dir = TempDir::new().unwrap(); + let input = temp_dir.path().join("input.bin"); + let output = temp_dir.path().join("output.bin"); + + create_test_file(&input, size_mb); + + let input_str = input.to_str().unwrap(); + let output_str = output.to_str().unwrap(); + + bencher.bench(|| { + remove_file(&output); + black_box(run_util_function( + uumain, + &[ + &format!("if={input_str}"), + &format!("of={output_str}"), + "bs=8K", + "status=none", + ], + )); + }); +} + +fn main() { + divan::main(); +} From 82b312abd3c5e362a2ba31ad1fa72c7894144d13 Mon Sep 17 00:00:00 2001 From: Etienne Cordonnier Date: Tue, 30 Dec 2025 22:54:50 +0100 Subject: [PATCH 008/112] nice: use Command::exec() instead of libc::execvp() (#9612) No need to use the unsafe `libc::execvp()`, the standard rust library provides the functionality via the safe function `Command::exec()`. Signed-off-by: Etienne Cordonnier Co-authored-by: Sylvestre Ledru --- src/uu/nice/src/nice.rs | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/src/uu/nice/src/nice.rs b/src/uu/nice/src/nice.rs index 8e47e9d078c..e689312875e 100644 --- a/src/uu/nice/src/nice.rs +++ b/src/uu/nice/src/nice.rs @@ -3,13 +3,14 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (ToDO) getpriority execvp setpriority nstr PRIO cstrs ENOENT +// spell-checker:ignore (ToDO) getpriority setpriority nstr PRIO use clap::{Arg, ArgAction, Command}; -use libc::{PRIO_PROCESS, c_char, c_int, execvp}; -use std::ffi::{CString, OsString}; -use std::io::{Error, Write}; -use std::ptr; +use libc::PRIO_PROCESS; +use std::ffi::OsString; +use std::io::{Error, ErrorKind, Write}; +use std::os::unix::process::CommandExt; +use std::process; use uucore::translate; use uucore::{ @@ -156,21 +157,15 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } } - let cstrs: Vec = matches - .get_many::(options::COMMAND) - .unwrap() - .map(|x| CString::new(x.as_bytes()).unwrap()) - .collect(); + let mut cmd_iter = matches.get_many::(options::COMMAND).unwrap(); + let cmd = cmd_iter.next().unwrap(); + let args: Vec<&String> = cmd_iter.collect(); - let mut args: Vec<*const c_char> = cstrs.iter().map(|s| s.as_ptr()).collect(); - args.push(ptr::null::()); - unsafe { - execvp(args[0], args.as_mut_ptr()); - } + let err = process::Command::new(cmd).args(args).exec(); - show_error!("execvp: {}", Error::last_os_error()); + show_error!("{}: {}", cmd, err); - let exit_code = if Error::last_os_error().raw_os_error().unwrap() as c_int == libc::ENOENT { + let exit_code = if err.kind() == ErrorKind::NotFound { 127 } else { 126 From 6f71e4df8d051bbc30bdabcb270a8ed6335186ce Mon Sep 17 00:00:00 2001 From: WaterWhisperer Date: Wed, 31 Dec 2025 06:53:43 +0800 Subject: [PATCH 009/112] du: fix -l/--count-links option not counting hardlinks separately (#9884) * du: fix -l/--count-links option not counting hardlinks separately * du: add test for -l/--count-links counting hardlinks separately --------- Co-authored-by: Sylvestre Ledru --- src/uu/du/src/du.rs | 13 ++----------- tests/by-util/test_du.rs | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 11 deletions(-) diff --git a/src/uu/du/src/du.rs b/src/uu/du/src/du.rs index 5fd824d61db..1b8084e2ebb 100644 --- a/src/uu/du/src/du.rs +++ b/src/uu/du/src/du.rs @@ -501,10 +501,7 @@ fn safe_du( // Handle inodes if let Some(inode) = this_stat.inode { - if seen_inodes.contains(&inode) && (!options.count_links || !options.all) { - if options.count_links && !options.all { - my_stat.inodes += 1; - } + if seen_inodes.contains(&inode) && !options.count_links { continue; } seen_inodes.insert(inode); @@ -660,13 +657,7 @@ fn du_regular( if let Some(inode) = this_stat.inode { // Check if the inode has been seen before and if we should skip it - if seen_inodes.contains(&inode) - && (!options.count_links || !options.all) - { - // If `count_links` is enabled and `all` is not, increment the inode count - if options.count_links && !options.all { - my_stat.inodes += 1; - } + if seen_inodes.contains(&inode) && !options.count_links { // Skip further processing for this inode continue; } diff --git a/tests/by-util/test_du.rs b/tests/by-util/test_du.rs index 01c612488c5..38d64d5b8b8 100644 --- a/tests/by-util/test_du.rs +++ b/tests/by-util/test_du.rs @@ -804,6 +804,44 @@ fn test_du_inodes_with_count_links_all() { assert_eq!(result_seq, ["1\td/d", "1\td/f", "1\td/h", "4\td"]); } +#[cfg(not(target_os = "android"))] +#[test] +fn test_du_count_links_hardlinks_separately() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + + at.mkdir("dir"); + at.touch("dir/file"); + at.hard_link("dir/file", "dir/hard_link"); + + let result_without_l = ts.ucmd().arg("-b").arg("dir").succeeds(); + let size_without_l: u64 = result_without_l + .stdout_str() + .split('\t') + .next() + .unwrap() + .trim() + .parse() + .unwrap(); + + for arg in ["-l", "--count-links"] { + let result_with_l = ts.ucmd().arg("-b").arg(arg).arg("dir").succeeds(); + let size_with_l: u64 = result_with_l + .stdout_str() + .split('\t') + .next() + .unwrap() + .trim() + .parse() + .unwrap(); + + assert!( + size_with_l >= size_without_l, + "With {arg}, size ({size_with_l}) should be >= size without -l ({size_without_l})" + ); + } +} + #[test] fn test_du_h_flag_empty_file() { new_ucmd!() From d90e54a2e256909e9add2988922438cad80ad91c Mon Sep 17 00:00:00 2001 From: Chris Dryden Date: Tue, 30 Dec 2025 18:04:42 -0500 Subject: [PATCH 010/112] cp: fix preserve-gid when canonicalize fails due to inaccessible parent dirs (#9803) Co-authored-by: Sylvestre Ledru --- src/uu/cp/src/cp.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index 036e9f9ee55..1502a7ada72 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -2510,11 +2510,13 @@ fn copy_file( } if options.dereference(source_in_command_line) { - if let Ok(src) = canonicalize(source, MissingHandling::Normal, ResolveMode::Physical) { - if src.exists() { - copy_attributes(&src, dest, &options.attributes)?; - } - } + // Try to canonicalize, but if it fails (e.g., due to inaccessible parent directories), + // fall back to the original source path + let src_for_attrs = canonicalize(source, MissingHandling::Normal, ResolveMode::Physical) + .ok() + .filter(|p| p.exists()) + .unwrap_or_else(|| source.to_path_buf()); + copy_attributes(&src_for_attrs, dest, &options.attributes)?; } else if source_is_stream && !source.exists() { // Some stream files may not exist after we have copied it, // like anonymous pipes. Thus, we can't really copy its From e442bfc7dc0bf2c0bef0aeaf0a8a7f5ab10ec4f8 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Tue, 30 Dec 2025 22:56:25 +0100 Subject: [PATCH 011/112] nice: simplify the code --- src/uu/nice/src/nice.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uu/nice/src/nice.rs b/src/uu/nice/src/nice.rs index e689312875e..fc1e9057bf9 100644 --- a/src/uu/nice/src/nice.rs +++ b/src/uu/nice/src/nice.rs @@ -163,7 +163,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let err = process::Command::new(cmd).args(args).exec(); - show_error!("{}: {}", cmd, err); + show_error!("{cmd}: {err}"); let exit_code = if err.kind() == ErrorKind::NotFound { 127 From 0599920fb88e79fbb4dbd3bc767c8c3b9b224922 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 30 Dec 2025 17:38:49 +0000 Subject: [PATCH 012/112] chore(deps): update rust crate self_cell to v1.2.2 --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 909c0351005..5c62830e0da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2471,9 +2471,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "self_cell" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16c2f82143577edb4921b71ede051dac62ca3c16084e918bf7b40c96ae10eb33" +checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" [[package]] name = "selinux" From 48173a078faab7d1062b3a21d055b7ed3aca77a9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 30 Dec 2025 17:38:43 +0000 Subject: [PATCH 013/112] chore(deps): update rust crate clap_complete to v4.5.64 --- Cargo.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5c62830e0da..0d84035b97d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -367,9 +367,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.62" +version = "4.5.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "004eef6b14ce34759aa7de4aea3217e368f463f46a3ed3764ca4b5a4404003b4" +checksum = "4c0da80818b2d95eca9aa614a30783e42f62bf5fdfee24e68cfb960b071ba8d1" dependencies = [ "clap", ] @@ -1575,7 +1575,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde_core", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1873,7 +1873,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]] @@ -2439,7 +2439,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.11.0", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2745,7 +2745,7 @@ dependencies = [ "getrandom 0.3.3", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4410,7 +4410,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]] From 15c9033510a75d9286d5260f97727061604293d0 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Wed, 31 Dec 2025 22:24:35 +0900 Subject: [PATCH 014/112] Merge pull request #9943 from oech3/patch-4 GnuTests.yml: Stop manpage generation to reduce size of log --- util/build-gnu.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/util/build-gnu.sh b/util/build-gnu.sh index 15c72720fc8..734cde88cbd 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -125,6 +125,8 @@ if test -f gnu-built; then else # Disable useless checks "${SED}" -i 's|check-texinfo: $(syntax_checks)|check-texinfo:|' doc/local.mk + # Stop manpage generation for cleaner log + : > man/local.mk # Use CFLAGS for best build time since we discard GNU coreutils CFLAGS="${CFLAGS} -pipe -O0 -s" ./configure -C --quiet --disable-gcc-warnings --disable-nls --disable-dependency-tracking --disable-bold-man-page-references \ --enable-single-binary=symlinks --enable-install-program="arch,kill,uptime,hostname" \ From a4fee185331cf6023cf1e3646ba05ef845812b2c Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Wed, 31 Dec 2025 22:25:02 +0900 Subject: [PATCH 015/112] CICD.yml: Avoid no space left again (#9939) --- .github/workflows/CICD.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index f0d2c848220..e6a7fd4509e 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -814,6 +814,8 @@ jobs: if: matrix.job.skip-tests != true shell: bash run: | + command -v sudo && sudo rm -rf /usr/local/lib/android /usr/share/dotnet # avoid no space left + df -h ||: ## Test individual utilities ${{ steps.vars.outputs.CARGO_CMD }} ${{ steps.vars.outputs.CARGO_CMD_OPTIONS }} test --target=${{ matrix.job.target }} \ ${{ matrix.job.cargo-options }} ${{ steps.dep_vars.outputs.CARGO_UTILITY_LIST_OPTIONS }} From 1baa87f69bc2c82b4a3aa7163d64ec6af039c450 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Tue, 30 Dec 2025 18:43:36 +0100 Subject: [PATCH 016/112] benchmark: move some functions in uucore --- src/uu/cp/benches/cp_bench.rs | 26 +++--------- src/uu/dd/benches/dd_bench.rs | 54 +++++++++--------------- src/uucore/src/lib/features/benchmark.rs | 40 ++++++++++++++++++ 3 files changed, 64 insertions(+), 56 deletions(-) diff --git a/src/uu/cp/benches/cp_bench.rs b/src/uu/cp/benches/cp_bench.rs index ba29596d93c..d673c14e48c 100644 --- a/src/uu/cp/benches/cp_bench.rs +++ b/src/uu/cp/benches/cp_bench.rs @@ -4,24 +4,11 @@ // file that was distributed with this source code. use divan::{Bencher, black_box}; -use std::fs::{self, File}; -use std::io::Write; +use std::fs; use std::path::Path; use tempfile::TempDir; use uu_cp::uumain; -use uucore::benchmark::{fs_tree, run_util_function}; - -fn remove_path(path: &Path) { - if !path.exists() { - return; - } - - if path.is_dir() { - fs::remove_dir_all(path).unwrap(); - } else { - fs::remove_file(path).unwrap(); - } -} +use uucore::benchmark::{binary_data, fs_tree, fs_utils, run_util_function}; fn bench_cp_directory(bencher: Bencher, args: &[&str], setup_source: F) where @@ -38,7 +25,7 @@ where let dest_str = dest.to_str().unwrap(); bencher.bench(|| { - remove_path(&dest); + fs_utils::remove_path(&dest); let mut full_args = Vec::with_capacity(args.len() + 2); full_args.extend_from_slice(args); @@ -99,16 +86,13 @@ fn cp_large_file(bencher: Bencher, size_mb: usize) { let source = temp_dir.path().join("source.bin"); let dest = temp_dir.path().join("dest.bin"); - let buffer = vec![b'x'; size_mb * 1024 * 1024]; - let mut file = File::create(&source).unwrap(); - file.write_all(&buffer).unwrap(); - file.sync_all().unwrap(); + binary_data::create_file(&source, size_mb, b'x'); let source_str = source.to_str().unwrap(); let dest_str = dest.to_str().unwrap(); bencher.bench(|| { - remove_path(&dest); + fs_utils::remove_path(&dest); black_box(run_util_function(uumain, &[source_str, dest_str])); }); diff --git a/src/uu/dd/benches/dd_bench.rs b/src/uu/dd/benches/dd_bench.rs index 0a86f5de18b..6e11ee7feb7 100644 --- a/src/uu/dd/benches/dd_bench.rs +++ b/src/uu/dd/benches/dd_bench.rs @@ -4,25 +4,9 @@ // file that was distributed with this source code. use divan::{Bencher, black_box}; -use std::fs::{self, File}; -use std::io::Write; -use std::path::Path; use tempfile::TempDir; use uu_dd::uumain; -use uucore::benchmark::run_util_function; - -fn create_test_file(path: &Path, size_mb: usize) { - let buffer = vec![b'x'; size_mb * 1024 * 1024]; - let mut file = File::create(path).unwrap(); - file.write_all(&buffer).unwrap(); - file.sync_all().unwrap(); -} - -fn remove_file(path: &Path) { - if path.exists() { - fs::remove_file(path).unwrap(); - } -} +use uucore::benchmark::{binary_data, fs_utils, run_util_function}; /// Benchmark basic dd copy with default settings #[divan::bench(args = [32])] @@ -31,13 +15,13 @@ fn dd_copy_default(bencher: Bencher, size_mb: usize) { let input = temp_dir.path().join("input.bin"); let output = temp_dir.path().join("output.bin"); - create_test_file(&input, size_mb); + binary_data::create_file(&input, size_mb, b'x'); let input_str = input.to_str().unwrap(); let output_str = output.to_str().unwrap(); bencher.bench(|| { - remove_file(&output); + fs_utils::remove_path(&output); black_box(run_util_function( uumain, &[ @@ -56,13 +40,13 @@ fn dd_copy_4k_blocks(bencher: Bencher, size_mb: usize) { let input = temp_dir.path().join("input.bin"); let output = temp_dir.path().join("output.bin"); - create_test_file(&input, size_mb); + binary_data::create_file(&input, size_mb, b'x'); let input_str = input.to_str().unwrap(); let output_str = output.to_str().unwrap(); bencher.bench(|| { - remove_file(&output); + fs_utils::remove_path(&output); black_box(run_util_function( uumain, &[ @@ -82,13 +66,13 @@ fn dd_copy_64k_blocks(bencher: Bencher, size_mb: usize) { let input = temp_dir.path().join("input.bin"); let output = temp_dir.path().join("output.bin"); - create_test_file(&input, size_mb); + binary_data::create_file(&input, size_mb, b'x'); let input_str = input.to_str().unwrap(); let output_str = output.to_str().unwrap(); bencher.bench(|| { - remove_file(&output); + fs_utils::remove_path(&output); black_box(run_util_function( uumain, &[ @@ -108,13 +92,13 @@ fn dd_copy_1m_blocks(bencher: Bencher, size_mb: usize) { let input = temp_dir.path().join("input.bin"); let output = temp_dir.path().join("output.bin"); - create_test_file(&input, size_mb); + binary_data::create_file(&input, size_mb, b'x'); let input_str = input.to_str().unwrap(); let output_str = output.to_str().unwrap(); bencher.bench(|| { - remove_file(&output); + fs_utils::remove_path(&output); black_box(run_util_function( uumain, &[ @@ -134,13 +118,13 @@ fn dd_copy_separate_blocks(bencher: Bencher, size_mb: usize) { let input = temp_dir.path().join("input.bin"); let output = temp_dir.path().join("output.bin"); - create_test_file(&input, size_mb); + binary_data::create_file(&input, size_mb, b'x'); let input_str = input.to_str().unwrap(); let output_str = output.to_str().unwrap(); bencher.bench(|| { - remove_file(&output); + fs_utils::remove_path(&output); black_box(run_util_function( uumain, &[ @@ -161,13 +145,13 @@ fn dd_copy_partial(bencher: Bencher, size_mb: usize) { let input = temp_dir.path().join("input.bin"); let output = temp_dir.path().join("output.bin"); - create_test_file(&input, size_mb); + binary_data::create_file(&input, size_mb, b'x'); let input_str = input.to_str().unwrap(); let output_str = output.to_str().unwrap(); bencher.bench(|| { - remove_file(&output); + fs_utils::remove_path(&output); black_box(run_util_function( uumain, &[ @@ -188,13 +172,13 @@ fn dd_copy_with_skip(bencher: Bencher, size_mb: usize) { let input = temp_dir.path().join("input.bin"); let output = temp_dir.path().join("output.bin"); - create_test_file(&input, size_mb); + binary_data::create_file(&input, size_mb, b'x'); let input_str = input.to_str().unwrap(); let output_str = output.to_str().unwrap(); bencher.bench(|| { - remove_file(&output); + fs_utils::remove_path(&output); black_box(run_util_function( uumain, &[ @@ -215,13 +199,13 @@ fn dd_copy_with_seek(bencher: Bencher, size_mb: usize) { let input = temp_dir.path().join("input.bin"); let output = temp_dir.path().join("output.bin"); - create_test_file(&input, size_mb); + binary_data::create_file(&input, size_mb, b'x'); let input_str = input.to_str().unwrap(); let output_str = output.to_str().unwrap(); bencher.bench(|| { - remove_file(&output); + fs_utils::remove_path(&output); black_box(run_util_function( uumain, &[ @@ -242,13 +226,13 @@ fn dd_copy_8k_blocks(bencher: Bencher, size_mb: usize) { let input = temp_dir.path().join("input.bin"); let output = temp_dir.path().join("output.bin"); - create_test_file(&input, size_mb); + binary_data::create_file(&input, size_mb, b'x'); let input_str = input.to_str().unwrap(); let output_str = output.to_str().unwrap(); bencher.bench(|| { - remove_file(&output); + fs_utils::remove_path(&output); black_box(run_util_function( uumain, &[ diff --git a/src/uucore/src/lib/features/benchmark.rs b/src/uucore/src/lib/features/benchmark.rs index 306ffdc3da7..8be0baf720a 100644 --- a/src/uucore/src/lib/features/benchmark.rs +++ b/src/uucore/src/lib/features/benchmark.rs @@ -289,6 +289,46 @@ pub mod text_data { } } +/// Binary data generation utilities for benchmarking +pub mod binary_data { + use std::fs::File; + use std::io::Write; + use std::path::Path; + + /// Create a binary file filled with a repeated pattern + /// + /// Creates a file of the specified size (in MB) filled with the given byte pattern. + /// This is useful for benchmarking utilities that work with large binary files like dd, cp, etc. + pub fn create_file(path: &Path, size_mb: usize, pattern: u8) { + let buffer = vec![pattern; size_mb * 1024 * 1024]; + let mut file = File::create(path).unwrap(); + file.write_all(&buffer).unwrap(); + file.sync_all().unwrap(); + } +} + +/// Filesystem utilities for benchmarking +pub mod fs_utils { + use std::fs; + use std::path::Path; + + /// Remove a file or directory if it exists + /// + /// This is a convenience function for cleaning up between benchmark iterations. + /// It handles both files and directories, and is a no-op if the path doesn't exist. + pub fn remove_path(path: &Path) { + if !path.exists() { + return; + } + + if path.is_dir() { + fs::remove_dir_all(path).unwrap(); + } else { + fs::remove_file(path).unwrap(); + } + } +} + /// Filesystem tree generation utilities for benchmarking pub mod fs_tree { use std::fs::{self, File}; From 71616c17172c60743e2ef672314ed40ed3e980a0 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Tue, 30 Dec 2025 18:47:31 +0100 Subject: [PATCH 017/112] benchmark: don't pass the args in the divan function --- src/uu/dd/benches/dd_bench.rs | 45 +++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/src/uu/dd/benches/dd_bench.rs b/src/uu/dd/benches/dd_bench.rs index 6e11ee7feb7..b08207e7ecc 100644 --- a/src/uu/dd/benches/dd_bench.rs +++ b/src/uu/dd/benches/dd_bench.rs @@ -9,8 +9,9 @@ use uu_dd::uumain; use uucore::benchmark::{binary_data, fs_utils, run_util_function}; /// Benchmark basic dd copy with default settings -#[divan::bench(args = [32])] -fn dd_copy_default(bencher: Bencher, size_mb: usize) { +#[divan::bench] +fn dd_copy_default(bencher: Bencher) { + let size_mb = 32; let temp_dir = TempDir::new().unwrap(); let input = temp_dir.path().join("input.bin"); let output = temp_dir.path().join("output.bin"); @@ -34,8 +35,9 @@ fn dd_copy_default(bencher: Bencher, size_mb: usize) { } /// Benchmark dd copy with 4KB block size (common page size) -#[divan::bench(args = [24])] -fn dd_copy_4k_blocks(bencher: Bencher, size_mb: usize) { +#[divan::bench] +fn dd_copy_4k_blocks(bencher: Bencher) { + let size_mb = 24; let temp_dir = TempDir::new().unwrap(); let input = temp_dir.path().join("input.bin"); let output = temp_dir.path().join("output.bin"); @@ -60,8 +62,9 @@ fn dd_copy_4k_blocks(bencher: Bencher, size_mb: usize) { } /// Benchmark dd copy with 64KB block size -#[divan::bench(args = [64])] -fn dd_copy_64k_blocks(bencher: Bencher, size_mb: usize) { +#[divan::bench] +fn dd_copy_64k_blocks(bencher: Bencher) { + let size_mb = 64; let temp_dir = TempDir::new().unwrap(); let input = temp_dir.path().join("input.bin"); let output = temp_dir.path().join("output.bin"); @@ -86,8 +89,9 @@ fn dd_copy_64k_blocks(bencher: Bencher, size_mb: usize) { } /// Benchmark dd copy with 1MB block size -#[divan::bench(args = [128])] -fn dd_copy_1m_blocks(bencher: Bencher, size_mb: usize) { +#[divan::bench] +fn dd_copy_1m_blocks(bencher: Bencher) { + let size_mb = 128; let temp_dir = TempDir::new().unwrap(); let input = temp_dir.path().join("input.bin"); let output = temp_dir.path().join("output.bin"); @@ -112,8 +116,9 @@ fn dd_copy_1m_blocks(bencher: Bencher, size_mb: usize) { } /// Benchmark dd copy with separate input and output block sizes -#[divan::bench(args = [48])] -fn dd_copy_separate_blocks(bencher: Bencher, size_mb: usize) { +#[divan::bench] +fn dd_copy_separate_blocks(bencher: Bencher) { + let size_mb = 48; let temp_dir = TempDir::new().unwrap(); let input = temp_dir.path().join("input.bin"); let output = temp_dir.path().join("output.bin"); @@ -139,8 +144,9 @@ fn dd_copy_separate_blocks(bencher: Bencher, size_mb: usize) { } /// Benchmark dd with count limit (partial copy) -#[divan::bench(args = [32])] -fn dd_copy_partial(bencher: Bencher, size_mb: usize) { +#[divan::bench] +fn dd_copy_partial(bencher: Bencher) { + let size_mb = 32; let temp_dir = TempDir::new().unwrap(); let input = temp_dir.path().join("input.bin"); let output = temp_dir.path().join("output.bin"); @@ -166,8 +172,9 @@ fn dd_copy_partial(bencher: Bencher, size_mb: usize) { } /// Benchmark dd with skip (seeking in input) -#[divan::bench(args = [48])] -fn dd_copy_with_skip(bencher: Bencher, size_mb: usize) { +#[divan::bench] +fn dd_copy_with_skip(bencher: Bencher) { + let size_mb = 48; let temp_dir = TempDir::new().unwrap(); let input = temp_dir.path().join("input.bin"); let output = temp_dir.path().join("output.bin"); @@ -193,8 +200,9 @@ fn dd_copy_with_skip(bencher: Bencher, size_mb: usize) { } /// Benchmark dd with seek (seeking in output) -#[divan::bench(args = [48])] -fn dd_copy_with_seek(bencher: Bencher, size_mb: usize) { +#[divan::bench] +fn dd_copy_with_seek(bencher: Bencher) { + let size_mb = 48; let temp_dir = TempDir::new().unwrap(); let input = temp_dir.path().join("input.bin"); let output = temp_dir.path().join("output.bin"); @@ -220,8 +228,9 @@ fn dd_copy_with_seek(bencher: Bencher, size_mb: usize) { } /// Benchmark dd with different block sizes for comparison -#[divan::bench(args = [32])] -fn dd_copy_8k_blocks(bencher: Bencher, size_mb: usize) { +#[divan::bench] +fn dd_copy_8k_blocks(bencher: Bencher) { + let size_mb = 32; let temp_dir = TempDir::new().unwrap(); let input = temp_dir.path().join("input.bin"); let output = temp_dir.path().join("output.bin"); From 4fbf9d1259cdd27caaad9a34b4c7781b9b6a03bb Mon Sep 17 00:00:00 2001 From: oech3 <> Date: Wed, 31 Dec 2025 22:39:51 +0900 Subject: [PATCH 018/112] GnuTests: Drop texinfo dep --- .github/workflows/GnuTests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/GnuTests.yml b/.github/workflows/GnuTests.yml index 4d312388b0c..8716cf04f1f 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) autopoint chksum dpkg getenforce getlimits gperf lcov libexpect limactl pyinotify setenforce shopt texinfo valgrind libattr libcap taiki-e zstd cpio +# spell-checker:ignore (libs/utils) autopoint chksum dpkg getenforce getlimits 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 @@ -247,7 +247,7 @@ jobs: - name: Install dependencies in VM run: | lima sudo dnf -y update - lima sudo dnf -y install git autoconf autopoint bison texinfo gperf gcc gdb jq libacl-devel libattr-devel libcap-devel libselinux-devel attr rustup clang-devel texinfo-tex automake patch quilt + lima sudo dnf -y install git autoconf autopoint bison gperf gcc gdb jq libacl-devel libattr-devel libcap-devel libselinux-devel attr rustup clang-devel automake patch quilt lima rustup-init -y --default-toolchain stable - name: Copy the sources to VM run: | From 5e82ac5b035534abdb7a1a5c4c39ab49897904d3 Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Wed, 31 Dec 2025 18:14:10 +0100 Subject: [PATCH 019/112] GnuTests.yml: install Rust without rustfmt (#9947) --- .github/workflows/GnuTests.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/GnuTests.yml b/.github/workflows/GnuTests.yml index 8716cf04f1f..9298d3d8518 100644 --- a/.github/workflows/GnuTests.yml +++ b/.github/workflows/GnuTests.yml @@ -47,7 +47,6 @@ jobs: - uses: dtolnay/rust-toolchain@master with: toolchain: stable - components: rustfmt - uses: Swatinem/rust-cache@v2 with: workspaces: "./uutils -> target" @@ -105,7 +104,7 @@ jobs: ## Build binaries cd 'uutils' env PROFILE=release-small bash util/build-gnu.sh - + - name: Save files for faster configure and skipping make uses: actions/cache/save@v5 if: always() && steps.cache-config-gnu.outputs.cache-hit != 'true' @@ -211,7 +210,6 @@ jobs: - uses: dtolnay/rust-toolchain@master with: toolchain: stable - components: rustfmt - uses: Swatinem/rust-cache@v2 with: workspaces: "./uutils -> target" @@ -331,7 +329,6 @@ jobs: - uses: dtolnay/rust-toolchain@master with: toolchain: stable - components: rustfmt - uses: Swatinem/rust-cache@v2 with: workspaces: "./uutils -> target" From d7a258fce52e41ba8eb4e3252a5d00a7abe6987c Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Tue, 30 Dec 2025 08:28:12 +0900 Subject: [PATCH 020/112] openbsd.yml: Replace cargo related cache deletion --- .github/workflows/openbsd.yml | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/.github/workflows/openbsd.yml b/.github/workflows/openbsd.yml index 8bb91566a02..7a14240c828 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 - rm -rf /usr/share/doc/* /usr/share/man/* /var/cache/* /tmp/* || true + rm -rf /usr/share/relink/* /usr/X11R6/* /usr/share/doc/* /usr/share/man/* || : pkg_add curl sudo-- jq coreutils bash rust rust-clippy rust-rustfmt llvm-- # Clean up package cache after installation pkg_delete -a || true @@ -115,8 +115,6 @@ jobs: fi # Clean to avoid to rsync back the files and free up disk space cargo clean - # Additional cleanup to free disk space - rm -rf ~/.cargo/registry/cache ~/.cargo/git/db || true if [ -n "\${FAIL_ON_FAULT}" ] && [ -n "\${FAULT}" ]; then exit 1 ; fi EOF @@ -144,10 +142,10 @@ jobs: prepare: | # Clean up disk space before installing packages df -h - rm -rf /usr/share/doc/* /usr/share/man/* /var/cache/* /tmp/* || true + rm -rf /usr/share/relink/* /usr/X11R6/* /usr/share/doc/* /usr/share/man/* || : pkg_add curl gmake sudo-- jq rust llvm-- # Clean up package cache after installation - pkg_delete -a || true + pkg_delete -a || : df -h run: | ## Prepare, build, and test @@ -197,8 +195,6 @@ jobs: cd "${WORKSPACE}" unset FAULT cargo build || FAULT=1 - # Clean build artifacts to save disk space before testing - rm -rf target/debug/build target/debug/incremental || true export PATH=~/.cargo/bin:${PATH} export RUST_BACKTRACE=1 export CARGO_TERM_COLOR=always @@ -216,6 +212,5 @@ jobs: # Clean to avoid to rsync back the files and free up disk space cargo clean # Additional cleanup to free disk space - rm -rf ~/.cargo/registry/cache ~/.cargo/git/db target/debug/deps target/release/deps || true if (test -n "\$FAULT"); then exit 1 ; fi EOF From 89f77185f0f9c2a7469bd0b4d349e0a0d16c732a Mon Sep 17 00:00:00 2001 From: cerdelen <95369756+cerdelen@users.noreply.github.com> Date: Wed, 31 Dec 2025 20:01:12 +0100 Subject: [PATCH 021/112] tac: fix error message (#9942) * tac: fix error message * tac: Remove obsolete error messages from locales --- src/uu/tac/locales/en-US.ftl | 2 +- src/uu/tac/locales/fr-FR.ftl | 2 +- src/uu/tac/src/error.rs | 6 +++--- src/uu/tac/src/tac.rs | 3 ++- tests/by-util/test_tac.rs | 2 +- 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/uu/tac/locales/en-US.ftl b/src/uu/tac/locales/en-US.ftl index 3c849c4d712..2632aa3dbb8 100644 --- a/src/uu/tac/locales/en-US.ftl +++ b/src/uu/tac/locales/en-US.ftl @@ -6,7 +6,7 @@ tac-help-separator = use STRING as the separator instead of newline # Error messages tac-error-invalid-regex = invalid regular expression: { $error } -tac-error-invalid-argument = { $argument }: read error: Invalid argument +tac-error-invalid-directory-argument = { $argument }: read error: Is a directory tac-error-file-not-found = failed to open { $filename } for reading: No such file or directory tac-error-read-error = failed to read from { $filename }: { $error } tac-error-write-error = failed to write to stdout: { $error } diff --git a/src/uu/tac/locales/fr-FR.ftl b/src/uu/tac/locales/fr-FR.ftl index f49a39e8d19..6c56de6283a 100644 --- a/src/uu/tac/locales/fr-FR.ftl +++ b/src/uu/tac/locales/fr-FR.ftl @@ -6,7 +6,7 @@ tac-help-separator = utiliser CHAÎNE comme séparateur au lieu du saut de ligne # Messages d'erreur tac-error-invalid-regex = expression régulière invalide : { $error } -tac-error-invalid-argument = { $argument } : erreur de lecture : Argument invalide tac-error-file-not-found = échec de l'ouverture de { $filename } en lecture : Aucun fichier ou répertoire de ce type tac-error-read-error = échec de la lecture depuis { $filename } : { $error } tac-error-write-error = échec de l'écriture vers stdout : { $error } +tac-error-invalid-directory-argument = { $argument } : erreur de lecture : Est un répertoire diff --git a/src/uu/tac/src/error.rs b/src/uu/tac/src/error.rs index 133a46266a0..098e997d4af 100644 --- a/src/uu/tac/src/error.rs +++ b/src/uu/tac/src/error.rs @@ -15,9 +15,9 @@ pub enum TacError { /// A regular expression given by the user is invalid. #[error("{}", translate!("tac-error-invalid-regex", "error" => .0))] InvalidRegex(regex::Error), - /// An argument to tac is invalid. - #[error("{}", translate!("tac-error-invalid-argument", "argument" => .0.maybe_quote()))] - InvalidArgument(OsString), + /// The argument to tac is a directory. + #[error("{}", translate!("tac-error-invalid-directory-argument", "argument" => .0.maybe_quote()))] + InvalidDirectoryArgument(OsString), /// The specified file is not found on the filesystem. #[error("{}", translate!("tac-error-file-not-found", "filename" => .0.quote()))] FileNotFound(OsString), diff --git a/src/uu/tac/src/tac.rs b/src/uu/tac/src/tac.rs index 507dd153199..f38661d03e9 100644 --- a/src/uu/tac/src/tac.rs +++ b/src/uu/tac/src/tac.rs @@ -253,7 +253,8 @@ fn tac(filenames: &[OsString], before: bool, regex: bool, separator: &str) -> UR } else { let path = Path::new(filename); if path.is_dir() { - let e: Box = TacError::InvalidArgument(filename.clone()).into(); + let e: Box = + TacError::InvalidDirectoryArgument(filename.clone()).into(); show!(e); continue; } diff --git a/tests/by-util/test_tac.rs b/tests/by-util/test_tac.rs index 0f5aad48808..feb79f581e4 100644 --- a/tests/by-util/test_tac.rs +++ b/tests/by-util/test_tac.rs @@ -100,7 +100,7 @@ fn test_invalid_input() { .ucmd() .arg("a") .fails() - .stderr_contains("a: read error: Invalid argument"); + .stderr_contains("a: read error: Is a directory"); } #[test] From 91d8861e37ca83e344af5831ce7bbd0ed09eb452 Mon Sep 17 00:00:00 2001 From: Chris Dryden Date: Wed, 31 Dec 2025 14:02:05 -0500 Subject: [PATCH 022/112] stty: use stdin for TTY operations instead of /dev/tty (#9881) * stty: use stdin for TTY operations instead of /dev/tty * Add tests for stty stdin behavior and fix platform-specific error messages --------- Co-authored-by: Sylvestre Ledru --- .../workspace.wordlist.txt | 1 + src/uu/stty/src/stty.rs | 71 +++++++++---------- tests/by-util/test_stty.rs | 70 ++++++++++++++++++ 3 files changed, 105 insertions(+), 37 deletions(-) diff --git a/.vscode/cspell.dictionaries/workspace.wordlist.txt b/.vscode/cspell.dictionaries/workspace.wordlist.txt index 8a8a1474a92..f9c8d686bff 100644 --- a/.vscode/cspell.dictionaries/workspace.wordlist.txt +++ b/.vscode/cspell.dictionaries/workspace.wordlist.txt @@ -379,6 +379,7 @@ istrip litout opost parodd +ENOTTY # translation tests CLICOLOR diff --git a/src/uu/stty/src/stty.rs b/src/uu/stty/src/stty.rs index d60d4d985ba..9153c1528fe 100644 --- a/src/uu/stty/src/stty.rs +++ b/src/uu/stty/src/stty.rs @@ -26,12 +26,12 @@ use nix::sys::termios::{ use nix::{ioctl_read_bad, ioctl_write_ptr_bad}; use std::cmp::Ordering; use std::fs::File; -use std::io::{self, Stdout, stdout}; +use std::io::{self, Stdin, stdin, stdout}; use std::num::IntErrorKind; use std::os::fd::{AsFd, BorrowedFd}; use std::os::unix::fs::OpenOptionsExt; use std::os::unix::io::{AsRawFd, RawFd}; -use uucore::error::{UError, UResult, USimpleError, UUsageError}; +use uucore::error::{FromIo, UError, UResult, USimpleError, UUsageError}; use uucore::format_usage; use uucore::parser::num_parser::ExtendedParser; use uucore::translate; @@ -124,12 +124,13 @@ struct Options<'a> { all: bool, save: bool, file: Device, + device_name: String, settings: Option>, } enum Device { File(File), - Stdout(Stdout), + Stdin(Stdin), } #[derive(Debug)] @@ -166,7 +167,7 @@ impl AsFd for Device { fn as_fd(&self) -> BorrowedFd<'_> { match self { Self::File(f) => f.as_fd(), - Self::Stdout(stdout) => stdout.as_fd(), + Self::Stdin(stdin) => stdin.as_fd(), } } } @@ -175,45 +176,42 @@ impl AsRawFd for Device { fn as_raw_fd(&self) -> RawFd { match self { Self::File(f) => f.as_raw_fd(), - Self::Stdout(stdout) => stdout.as_raw_fd(), + Self::Stdin(stdin) => stdin.as_raw_fd(), } } } impl<'a> Options<'a> { fn from(matches: &'a ArgMatches) -> io::Result { - Ok(Self { - all: matches.get_flag(options::ALL), - save: matches.get_flag(options::SAVE), - file: match matches.get_one::(options::FILE) { - // Two notes here: - // 1. O_NONBLOCK is needed because according to GNU docs, a - // POSIX tty can block waiting for carrier-detect if the - // "clocal" flag is not set. If your TTY is not connected - // to a modem, it is probably not relevant though. - // 2. We never close the FD that we open here, but the OS - // will clean up the FD for us on exit, so it doesn't - // matter. The alternative would be to have an enum of - // BorrowedFd/OwnedFd to handle both cases. - Some(f) => Device::File( + let (file, device_name) = match matches.get_one::(options::FILE) { + // Two notes here: + // 1. O_NONBLOCK is needed because according to GNU docs, a + // POSIX tty can block waiting for carrier-detect if the + // "clocal" flag is not set. If your TTY is not connected + // to a modem, it is probably not relevant though. + // 2. We never close the FD that we open here, but the OS + // will clean up the FD for us on exit, so it doesn't + // matter. The alternative would be to have an enum of + // BorrowedFd/OwnedFd to handle both cases. + Some(f) => ( + Device::File( std::fs::OpenOptions::new() .read(true) .custom_flags(O_NONBLOCK) .open(f)?, ), - // default to /dev/tty, if that does not exist then default to stdout - None => { - if let Ok(f) = std::fs::OpenOptions::new() - .read(true) - .custom_flags(O_NONBLOCK) - .open("/dev/tty") - { - Device::File(f) - } else { - Device::Stdout(stdout()) - } - } - }, + f.clone(), + ), + // Per POSIX, stdin is used for TTY operations when no device is specified. + // This matches GNU coreutils behavior: if stdin is not a TTY, + // tcgetattr will fail with "Inappropriate ioctl for device". + None => (Device::Stdin(stdin()), "standard input".to_string()), + }; + Ok(Self { + all: matches.get_flag(options::ALL), + save: matches.get_flag(options::SAVE), + file, + device_name, settings: matches .get_many::(options::SETTINGS) .map(|v| v.map(|s| s.as_ref()).collect()), @@ -412,8 +410,8 @@ fn stty(opts: &Options) -> UResult<()> { } } - // TODO: Figure out the right error message for when tcgetattr fails - let mut termios = tcgetattr(opts.file.as_fd())?; + let mut termios = + tcgetattr(opts.file.as_fd()).map_err_context(|| opts.device_name.clone())?; // iterate over valid_args, match on the arg type, do the matching apply function for arg in &valid_args { @@ -433,8 +431,7 @@ fn stty(opts: &Options) -> UResult<()> { } tcsetattr(opts.file.as_fd(), set_arg, &termios)?; } else { - // TODO: Figure out the right error message for when tcgetattr fails - let termios = tcgetattr(opts.file.as_fd())?; + let termios = tcgetattr(opts.file.as_fd()).map_err_context(|| opts.device_name.clone())?; print_settings(&termios, opts)?; } Ok(()) @@ -997,7 +994,7 @@ fn apply_char_mapping(termios: &mut Termios, mapping: &(S, u8)) { /// /// The state array contains: /// - `state[0]`: input flags -/// - `state[1]`: output flags +/// - `state[1]`: output flags /// - `state[2]`: control flags /// - `state[3]`: local flags /// - `state[4..]`: control characters (optional) diff --git a/tests/by-util/test_stty.rs b/tests/by-util/test_stty.rs index 136ea2768af..ae64eb6aeb5 100644 --- a/tests/by-util/test_stty.rs +++ b/tests/by-util/test_stty.rs @@ -1557,6 +1557,76 @@ fn test_saved_state_with_control_chars() { .code_is(exp_result.code()); } +// Per POSIX, stty uses stdin for TTY operations. When stdin is a pipe, it should fail. +#[test] +#[cfg(unix)] +fn test_stdin_not_tty_fails() { + // ENOTTY error message varies by platform/libc: + // - glibc: "Inappropriate ioctl for device" + // - musl: "Not a tty" + // - Android: "Not a typewriter" + #[cfg(target_os = "android")] + let expected_error = "standard input: Not a typewriter"; + #[cfg(all(not(target_os = "android"), target_env = "musl"))] + let expected_error = "standard input: Not a tty"; + #[cfg(all(not(target_os = "android"), not(target_env = "musl")))] + let expected_error = "standard input: Inappropriate ioctl for device"; + + new_ucmd!() + .pipe_in("") + .fails() + .stderr_contains(expected_error); +} + +// Test that stty uses stdin for TTY operations per POSIX. +// Verifies: output redirection (#8012), save/restore pattern (#8608), stdin redirection (#8848) +#[test] +#[cfg(unix)] +fn test_stty_uses_stdin() { + use std::fs::File; + use std::process::Stdio; + + let (path, _controller, _replica) = pty_path(); + + // Output redirection: stty > file (stdin is still TTY) + let stdin = File::open(&path).unwrap(); + new_ucmd!() + .set_stdin(stdin) + .set_stdout(Stdio::piped()) + .succeeds() + .stdout_contains("speed"); + + // Save/restore: stty $(stty -g) pattern + let stdin = File::open(&path).unwrap(); + let saved = new_ucmd!() + .arg("-g") + .set_stdin(stdin) + .set_stdout(Stdio::piped()) + .succeeds() + .stdout_str() + .trim() + .to_string(); + assert!(saved.contains(':'), "Expected colon-separated saved state"); + + let stdin = File::open(&path).unwrap(); + new_ucmd!().arg(&saved).set_stdin(stdin).succeeds(); + + // Stdin redirection: stty rows 30 cols 100 < /dev/pts/N + let stdin = File::open(&path).unwrap(); + new_ucmd!() + .args(&["rows", "30", "cols", "100"]) + .set_stdin(stdin) + .succeeds(); + + let stdin = File::open(&path).unwrap(); + new_ucmd!() + .arg("--all") + .set_stdin(stdin) + .succeeds() + .stdout_contains("rows 30") + .stdout_contains("columns 100"); +} + #[test] #[cfg(unix)] fn test_columns_env_wrapping() { From 21dda9285ffffd4f5b7b0eb842fc951dda1ca691 Mon Sep 17 00:00:00 2001 From: Ibrahim Burak Yorulmaz Date: Wed, 31 Dec 2025 20:09:14 +0100 Subject: [PATCH 023/112] Use libc::UTIME_NOW in touch when updating time to now (#9870) * Use libc::UTIME_NOW in touch when updating time to now * Explicit libc import to satisfy clippy * Fix location of cfg in touch * Add cfg gate to libc import to satisfy clippy on windows * Change cfg gates in touch near libc::UTIME_NOW from unix to linux --------- Co-authored-by: Sylvestre Ledru --- src/uu/touch/src/touch.rs | 18 ++++++++++++++++-- tests/by-util/test_touch.rs | 7 +++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/uu/touch/src/touch.rs b/src/uu/touch/src/touch.rs index e00c1df8256..bde22ab336a 100644 --- a/src/uu/touch/src/touch.rs +++ b/src/uu/touch/src/touch.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 (ToDO) filetime datetime lpszfilepath mktime DATETIME datelike timelike +// spell-checker:ignore (ToDO) filetime datetime lpszfilepath mktime DATETIME datelike timelike UTIME // spell-checker:ignore (FORMATS) MMDDhhmm YYYYMMDDHHMM YYMMDDHHMM YYYYMMDDHHMMS pub mod error; @@ -23,6 +23,8 @@ use std::io::{Error, ErrorKind}; use std::path::{Path, PathBuf}; use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError}; +#[cfg(target_os = "linux")] +use uucore::libc; use uucore::parser::shortcut_value_parser::ShortcutValueParser; use uucore::translate; use uucore::{format_usage, show}; @@ -377,7 +379,19 @@ pub fn touch(files: &[InputFile], opts: &Options) -> Result<(), TouchError> { (atime, mtime) } Source::Now => { - let now = datetime_to_filetime(&Local::now()); + let now: FileTime; + #[cfg(target_os = "linux")] + { + if opts.date.is_none() { + now = FileTime::from_unix_time(0, libc::UTIME_NOW as u32); + } else { + now = datetime_to_filetime(&Local::now()); + } + } + #[cfg(not(target_os = "linux"))] + { + now = datetime_to_filetime(&Local::now()); + } (now, now) } &Source::Timestamp(ts) => (ts, ts), diff --git a/tests/by-util/test_touch.rs b/tests/by-util/test_touch.rs index 68075867237..eb2b5c02f6f 100644 --- a/tests/by-util/test_touch.rs +++ b/tests/by-util/test_touch.rs @@ -1052,3 +1052,10 @@ fn test_touch_non_utf8_paths() { scene.ucmd().arg(non_utf8_name).succeeds().no_output(); assert!(std::fs::metadata(at.plus(non_utf8_name)).is_ok()); } + +#[test] +#[cfg(target_os = "linux")] +fn test_touch_dev_full() { + let (_, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["/dev/full"]).succeeds().no_output(); +} From b68847b409c4a1c904957918200be42974c3b052 Mon Sep 17 00:00:00 2001 From: Chris Dryden Date: Wed, 31 Dec 2025 14:09:39 -0500 Subject: [PATCH 024/112] csplit: detect and report write errors (#9855) * csplit: detect and report write errors * Add Rust integration tests for csplit write error detection * csplit: fix doc comment for finish_split --- src/uu/csplit/src/csplit.rs | 35 ++++++++++++++++++++++++----------- tests/by-util/test_csplit.rs | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 11 deletions(-) diff --git a/src/uu/csplit/src/csplit.rs b/src/uu/csplit/src/csplit.rs index 01cc4e0dc47..1c2978cea0f 100644 --- a/src/uu/csplit/src/csplit.rs +++ b/src/uu/csplit/src/csplit.rs @@ -127,7 +127,7 @@ where let ret = do_csplit(&mut split_writer, patterns_vec, &mut input_iter); // consume the rest, unless there was an error - if ret.is_ok() { + let ret = if ret.is_ok() { input_iter.rewind_buffer(); if let Some((_, line)) = input_iter.next() { // There is remaining input: create a final split and copy remainder @@ -136,14 +136,18 @@ where for (_, line) in input_iter { split_writer.writeln(&line?)?; } - split_writer.finish_split(); + split_writer.finish_split() } else if all_up_to_line && options.suppress_matched { // GNU semantics for integer patterns with --suppress-matched: // even if no remaining input, create a final (possibly empty) split split_writer.new_writer()?; - split_writer.finish_split(); + split_writer.finish_split() + } else { + Ok(()) } - } + } else { + ret + }; // delete files on error by default if ret.is_err() && !options.keep_files { split_writer.delete_all_splits()?; @@ -305,15 +309,24 @@ impl SplitWriter<'_> { /// /// # Errors /// - /// Some [`io::Error`] if the split could not be removed in case it should be elided. - fn finish_split(&mut self) { + /// Returns an error if flushing the writer fails. + fn finish_split(&mut self) -> Result<(), CsplitError> { if !self.dev_null { + // Flush the writer to ensure all data is written and errors are detected + if let Some(ref mut writer) = self.current_writer { + let file_name = self.options.split_name.get(self.counter - 1); + writer + .flush() + .map_err_context(|| file_name.clone()) + .map_err(CsplitError::from)?; + } if self.options.elide_empty_files && self.size == 0 { self.counter -= 1; } else if !self.options.quiet { println!("{}", self.size); } } + Ok(()) } /// Removes all the split files that were created. @@ -379,7 +392,7 @@ impl SplitWriter<'_> { } self.writeln(&line)?; } - self.finish_split(); + self.finish_split()?; ret } @@ -446,7 +459,7 @@ impl SplitWriter<'_> { self.writeln(&line?)?; } None => { - self.finish_split(); + self.finish_split()?; return Err(CsplitError::LineOutOfRange( pattern_as_str.to_string(), )); @@ -454,7 +467,7 @@ impl SplitWriter<'_> { } offset -= 1; } - self.finish_split(); + self.finish_split()?; // if we have to suppress one line after we take the next and do nothing if next_line_suppress_matched { @@ -495,7 +508,7 @@ impl SplitWriter<'_> { ); } - self.finish_split(); + self.finish_split()?; if input_iter.buffer_len() < offset_usize { return Err(CsplitError::LineOutOfRange(pattern_as_str.to_string())); } @@ -511,7 +524,7 @@ impl SplitWriter<'_> { } } - self.finish_split(); + self.finish_split()?; Err(CsplitError::MatchNotFound(pattern_as_str.to_string())) } } diff --git a/tests/by-util/test_csplit.rs b/tests/by-util/test_csplit.rs index bf46063109a..76c217a29bb 100644 --- a/tests/by-util/test_csplit.rs +++ b/tests/by-util/test_csplit.rs @@ -1551,3 +1551,35 @@ fn test_csplit_non_utf8_paths() { ucmd.arg(&filename).arg("3").succeeds(); } + +/// Test write error detection using /dev/full +#[test] +#[cfg(target_os = "linux")] +fn test_write_error_dev_full() { + let (at, mut ucmd) = at_and_ucmd!(); + at.symlink_file("/dev/full", "xx01"); + + ucmd.args(&["-", "2"]) + .pipe_in("1\n2\n") + .fails_with_code(1) + .stderr_contains("xx01: No space left on device"); + + // Files cleaned up by default + assert!(!at.file_exists("xx00")); +} + +/// Test write error with -k keeps files +#[test] +#[cfg(target_os = "linux")] +fn test_write_error_dev_full_keep_files() { + let (at, mut ucmd) = at_and_ucmd!(); + at.symlink_file("/dev/full", "xx01"); + + ucmd.args(&["-k", "-", "2"]) + .pipe_in("1\n2\n") + .fails_with_code(1) + .stderr_contains("xx01: No space left on device"); + + assert!(at.file_exists("xx00")); + assert_eq!(at.read("xx00"), "1\n"); +} From d9d9a0ee78466ea81442ce27f3d46dedb1f109a9 Mon Sep 17 00:00:00 2001 From: Chris Dryden Date: Wed, 31 Dec 2025 14:16:17 -0500 Subject: [PATCH 025/112] fix(sort): split locale benchmarks into separate files per locale (#9914) --- src/uu/sort/Cargo.toml | 10 +- src/uu/sort/benches/sort_locale_bench.rs | 189 ------------------ src/uu/sort/benches/sort_locale_c_bench.rs | 72 +++++++ src/uu/sort/benches/sort_locale_de_bench.rs | 40 ++++ src/uu/sort/benches/sort_locale_utf8_bench.rs | 102 ++++++++++ 5 files changed, 223 insertions(+), 190 deletions(-) delete mode 100644 src/uu/sort/benches/sort_locale_bench.rs create mode 100644 src/uu/sort/benches/sort_locale_c_bench.rs create mode 100644 src/uu/sort/benches/sort_locale_de_bench.rs create mode 100644 src/uu/sort/benches/sort_locale_utf8_bench.rs diff --git a/src/uu/sort/Cargo.toml b/src/uu/sort/Cargo.toml index 184f6776be7..8a9570eaa30 100644 --- a/src/uu/sort/Cargo.toml +++ b/src/uu/sort/Cargo.toml @@ -60,5 +60,13 @@ name = "sort_bench" harness = false [[bench]] -name = "sort_locale_bench" +name = "sort_locale_c_bench" +harness = false + +[[bench]] +name = "sort_locale_utf8_bench" +harness = false + +[[bench]] +name = "sort_locale_de_bench" harness = false diff --git a/src/uu/sort/benches/sort_locale_bench.rs b/src/uu/sort/benches/sort_locale_bench.rs deleted file mode 100644 index d00ec9f4ac8..00000000000 --- a/src/uu/sort/benches/sort_locale_bench.rs +++ /dev/null @@ -1,189 +0,0 @@ -// 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 divan::{Bencher, black_box}; -use std::env; -use tempfile::NamedTempFile; -use uu_sort::uumain; -use uucore::benchmark::{run_util_function, setup_test_file, text_data}; - -/// Benchmark ASCII-only data sorting with C locale (byte comparison) -#[divan::bench] -fn sort_ascii_c_locale(bencher: Bencher) { - let data = text_data::generate_ascii_data_simple(100_000); - let file_path = setup_test_file(&data); - // Reuse the same output file across iterations to reduce filesystem variance - let output_file = NamedTempFile::new().unwrap(); - let output_path = output_file.path().to_str().unwrap().to_string(); - - bencher.bench(|| { - unsafe { - env::set_var("LC_ALL", "C"); - } - black_box(run_util_function( - uumain, - &["-o", &output_path, file_path.to_str().unwrap()], - )); - }); -} - -/// Benchmark ASCII-only data sorting with UTF-8 locale -#[divan::bench] -fn sort_ascii_utf8_locale(bencher: Bencher) { - let data = text_data::generate_ascii_data_simple(200_000); - let file_path = setup_test_file(&data); - // Reuse the same output file across iterations to reduce filesystem variance - let output_file = NamedTempFile::new().unwrap(); - let output_path = output_file.path().to_str().unwrap().to_string(); - - bencher.bench(|| { - unsafe { - env::set_var("LC_ALL", "en_US.UTF-8"); - } - black_box(run_util_function( - uumain, - &["-o", &output_path, file_path.to_str().unwrap()], - )); - }); -} - -/// Benchmark mixed ASCII/Unicode data with C locale -#[divan::bench] -fn sort_mixed_c_locale(bencher: Bencher) { - let data = text_data::generate_mixed_locale_data(50_000); - let file_path = setup_test_file(&data); - // Reuse the same output file across iterations to reduce filesystem variance - let output_file = NamedTempFile::new().unwrap(); - let output_path = output_file.path().to_str().unwrap().to_string(); - - bencher.bench(|| { - unsafe { - env::set_var("LC_ALL", "C"); - } - black_box(run_util_function( - uumain, - &["-o", &output_path, file_path.to_str().unwrap()], - )); - }); -} - -/// Benchmark mixed ASCII/Unicode data with UTF-8 locale -#[divan::bench] -fn sort_mixed_utf8_locale(bencher: Bencher) { - let data = text_data::generate_mixed_locale_data(50_000); - let file_path = setup_test_file(&data); - // Reuse the same output file across iterations to reduce filesystem variance - let output_file = NamedTempFile::new().unwrap(); - let output_path = output_file.path().to_str().unwrap().to_string(); - - bencher.bench(|| { - unsafe { - env::set_var("LC_ALL", "en_US.UTF-8"); - } - black_box(run_util_function( - uumain, - &["-o", &output_path, file_path.to_str().unwrap()], - )); - }); -} - -/// Benchmark German locale-specific data with C locale -#[divan::bench] -fn sort_german_c_locale(bencher: Bencher) { - let data = text_data::generate_german_locale_data(50_000); - let file_path = setup_test_file(&data); - // Reuse the same output file across iterations to reduce filesystem variance - let output_file = NamedTempFile::new().unwrap(); - let output_path = output_file.path().to_str().unwrap().to_string(); - - bencher.bench(|| { - unsafe { - env::set_var("LC_ALL", "C"); - } - black_box(run_util_function( - uumain, - &["-o", &output_path, file_path.to_str().unwrap()], - )); - }); -} - -/// Benchmark German locale-specific data with German locale -#[divan::bench] -fn sort_german_locale(bencher: Bencher) { - let data = text_data::generate_german_locale_data(50_000); - let file_path = setup_test_file(&data); - // Reuse the same output file across iterations to reduce filesystem variance - let output_file = NamedTempFile::new().unwrap(); - let output_path = output_file.path().to_str().unwrap().to_string(); - - bencher.bench(|| { - unsafe { - env::set_var("LC_ALL", "de_DE.UTF-8"); - } - black_box(run_util_function( - uumain, - &["-o", &output_path, file_path.to_str().unwrap()], - )); - }); -} - -/// Benchmark numeric sorting performance -#[divan::bench] -fn sort_numeric(bencher: Bencher) { - let mut data = Vec::new(); - for i in 0..50_000 { - let line = format!("{}\n", 50_000 - i); - data.extend_from_slice(line.as_bytes()); - } - let file_path = setup_test_file(&data); - - bencher.bench(|| { - unsafe { - env::set_var("LC_ALL", "en_US.UTF-8"); - } - black_box(run_util_function( - uumain, - &["-n", file_path.to_str().unwrap()], - )); - }); -} - -/// Benchmark reverse sorting -#[divan::bench] -fn sort_reverse_mixed(bencher: Bencher) { - let data = text_data::generate_mixed_locale_data(50_000); - let file_path = setup_test_file(&data); - - bencher.bench(|| { - unsafe { - env::set_var("LC_ALL", "en_US.UTF-8"); - } - black_box(run_util_function( - uumain, - &["-r", file_path.to_str().unwrap()], - )); - }); -} - -/// Benchmark unique sorting -#[divan::bench] -fn sort_unique_mixed(bencher: Bencher) { - let data = text_data::generate_mixed_locale_data(50_000); - let file_path = setup_test_file(&data); - - bencher.bench(|| { - unsafe { - env::set_var("LC_ALL", "en_US.UTF-8"); - } - black_box(run_util_function( - uumain, - &["-u", file_path.to_str().unwrap()], - )); - }); -} - -fn main() { - divan::main(); -} diff --git a/src/uu/sort/benches/sort_locale_c_bench.rs b/src/uu/sort/benches/sort_locale_c_bench.rs new file mode 100644 index 00000000000..378a2abb9ac --- /dev/null +++ b/src/uu/sort/benches/sort_locale_c_bench.rs @@ -0,0 +1,72 @@ +// 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. + +//! Benchmarks for sort with C locale (fast byte-wise comparison). +//! +//! Note: The locale is set in main() BEFORE any benchmark runs because +//! the locale is cached on first access via OnceLock and cannot be changed afterwards. + +use divan::{Bencher, black_box}; +use tempfile::NamedTempFile; +use uu_sort::uumain; +use uucore::benchmark::{run_util_function, setup_test_file, text_data}; + +/// Benchmark ASCII-only data sorting with C locale (byte comparison) +#[divan::bench] +fn sort_ascii_c_locale(bencher: Bencher) { + let data = text_data::generate_ascii_data_simple(100_000); + let file_path = setup_test_file(&data); + let output_file = NamedTempFile::new().unwrap(); + let output_path = output_file.path().to_str().unwrap().to_string(); + + bencher.bench(|| { + black_box(run_util_function( + uumain, + &["-o", &output_path, file_path.to_str().unwrap()], + )); + }); +} + +/// Benchmark mixed ASCII/Unicode data with C locale (byte comparison) +#[divan::bench] +fn sort_mixed_c_locale(bencher: Bencher) { + let data = text_data::generate_mixed_locale_data(50_000); + let file_path = setup_test_file(&data); + let output_file = NamedTempFile::new().unwrap(); + let output_path = output_file.path().to_str().unwrap().to_string(); + + bencher.bench(|| { + black_box(run_util_function( + uumain, + &["-o", &output_path, file_path.to_str().unwrap()], + )); + }); +} + +/// Benchmark German locale-specific data with C locale (byte comparison) +#[divan::bench] +fn sort_german_c_locale(bencher: Bencher) { + let data = text_data::generate_german_locale_data(50_000); + let file_path = setup_test_file(&data); + let output_file = NamedTempFile::new().unwrap(); + let output_path = output_file.path().to_str().unwrap().to_string(); + + bencher.bench(|| { + black_box(run_util_function( + uumain, + &["-o", &output_path, file_path.to_str().unwrap()], + )); + }); +} + +fn main() { + // Set C locale BEFORE any benchmarks run. + // This must happen before divan::main() because the locale is cached + // on first access via OnceLock and cannot be changed afterwards. + unsafe { + std::env::set_var("LC_ALL", "C"); + } + divan::main(); +} diff --git a/src/uu/sort/benches/sort_locale_de_bench.rs b/src/uu/sort/benches/sort_locale_de_bench.rs new file mode 100644 index 00000000000..5c760a694e8 --- /dev/null +++ b/src/uu/sort/benches/sort_locale_de_bench.rs @@ -0,0 +1,40 @@ +// 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. + +//! Benchmarks for sort with German locale (de_DE.UTF-8 collation). +//! +//! Note: The locale is set in main() BEFORE any benchmark runs because +//! the locale is cached on first access via OnceLock and cannot be changed afterwards. + +use divan::{Bencher, black_box}; +use tempfile::NamedTempFile; +use uu_sort::uumain; +use uucore::benchmark::{run_util_function, setup_test_file, text_data}; + +/// Benchmark German locale-specific data with German locale +#[divan::bench] +fn sort_german_de_locale(bencher: Bencher) { + let data = text_data::generate_german_locale_data(50_000); + let file_path = setup_test_file(&data); + let output_file = NamedTempFile::new().unwrap(); + let output_path = output_file.path().to_str().unwrap().to_string(); + + bencher.bench(|| { + black_box(run_util_function( + uumain, + &["-o", &output_path, file_path.to_str().unwrap()], + )); + }); +} + +fn main() { + // Set German locale BEFORE any benchmarks run. + // This must happen before divan::main() because the locale is cached + // on first access via OnceLock and cannot be changed afterwards. + unsafe { + std::env::set_var("LC_ALL", "de_DE.UTF-8"); + } + divan::main(); +} diff --git a/src/uu/sort/benches/sort_locale_utf8_bench.rs b/src/uu/sort/benches/sort_locale_utf8_bench.rs new file mode 100644 index 00000000000..b0ebb340d99 --- /dev/null +++ b/src/uu/sort/benches/sort_locale_utf8_bench.rs @@ -0,0 +1,102 @@ +// 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. + +//! Benchmarks for sort with UTF-8 locale (locale-aware collation). +//! +//! Note: The locale is set in main() BEFORE any benchmark runs because +//! the locale is cached on first access via OnceLock and cannot be changed afterwards. + +use divan::{Bencher, black_box}; +use tempfile::NamedTempFile; +use uu_sort::uumain; +use uucore::benchmark::{run_util_function, setup_test_file, text_data}; + +/// Benchmark ASCII-only data sorting with UTF-8 locale +#[divan::bench] +fn sort_ascii_utf8_locale(bencher: Bencher) { + let data = text_data::generate_ascii_data_simple(100_000); + let file_path = setup_test_file(&data); + let output_file = NamedTempFile::new().unwrap(); + let output_path = output_file.path().to_str().unwrap().to_string(); + + bencher.bench(|| { + black_box(run_util_function( + uumain, + &["-o", &output_path, file_path.to_str().unwrap()], + )); + }); +} + +/// Benchmark mixed ASCII/Unicode data with UTF-8 locale +#[divan::bench] +fn sort_mixed_utf8_locale(bencher: Bencher) { + let data = text_data::generate_mixed_locale_data(50_000); + let file_path = setup_test_file(&data); + let output_file = NamedTempFile::new().unwrap(); + let output_path = output_file.path().to_str().unwrap().to_string(); + + bencher.bench(|| { + black_box(run_util_function( + uumain, + &["-o", &output_path, file_path.to_str().unwrap()], + )); + }); +} + +/// Benchmark numeric sorting with UTF-8 locale +#[divan::bench] +fn sort_numeric_utf8_locale(bencher: Bencher) { + let mut data = Vec::new(); + for i in 0..50_000 { + let line = format!("{}\n", 50_000 - i); + data.extend_from_slice(line.as_bytes()); + } + let file_path = setup_test_file(&data); + + bencher.bench(|| { + black_box(run_util_function( + uumain, + &["-n", file_path.to_str().unwrap()], + )); + }); +} + +/// Benchmark reverse sorting with UTF-8 locale +#[divan::bench] +fn sort_reverse_utf8_locale(bencher: Bencher) { + let data = text_data::generate_mixed_locale_data(50_000); + let file_path = setup_test_file(&data); + + bencher.bench(|| { + black_box(run_util_function( + uumain, + &["-r", file_path.to_str().unwrap()], + )); + }); +} + +/// Benchmark unique sorting with UTF-8 locale +#[divan::bench] +fn sort_unique_utf8_locale(bencher: Bencher) { + let data = text_data::generate_mixed_locale_data(50_000); + let file_path = setup_test_file(&data); + + bencher.bench(|| { + black_box(run_util_function( + uumain, + &["-u", file_path.to_str().unwrap()], + )); + }); +} + +fn main() { + // Set UTF-8 locale BEFORE any benchmarks run. + // This must happen before divan::main() because the locale is cached + // on first access via OnceLock and cannot be changed afterwards. + unsafe { + std::env::set_var("LC_ALL", "en_US.UTF-8"); + } + divan::main(); +} From e15fd8919b0da2ad7627c23eb4eca6093e645709 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=E1=BA=A3=20th=E1=BA=BF=20gi=E1=BB=9Bi=20l=C3=A0=20Rust?= <90588855+naoNao89@users.noreply.github.com> Date: Thu, 1 Jan 2026 02:20:50 +0700 Subject: [PATCH 026/112] test(more): Fix test_from_line_option race condition by increasing PTY delay (#9629) * test(more): Fix test_from_line_option race condition by increasing PTY delay Increased the delay in run_more_with_pty() from 100ms to 500ms to allow more time to fully render output before the test reads from the PTY. The test was failing because it was reading too early, before more could initialize the terminal and render the file content. The -F flag itself works correctly. * test_more: decrease the delay --------- Co-authored-by: Sylvestre Ledru --- tests/by-util/test_more.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/by-util/test_more.rs b/tests/by-util/test_more.rs index 2bf130a1858..b5256af6174 100644 --- a/tests/by-util/test_more.rs +++ b/tests/by-util/test_more.rs @@ -35,7 +35,7 @@ fn run_more_with_pty( .arg(file) .run_no_wait(); - child.delay(100); + child.delay(200); let mut output = vec![0u8; 1024]; let n = read(&controller, &mut output).unwrap(); let output_str = String::from_utf8_lossy(&output[..n]).to_string(); From fbf849b2dc425dcd42a0f944b5c605cf9660ba69 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Thu, 1 Jan 2026 15:08:19 +0900 Subject: [PATCH 027/112] GnuTests.yml: Drop autopoint --- .github/workflows/GnuTests.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/GnuTests.yml b/.github/workflows/GnuTests.yml index 9298d3d8518..d50074b901c 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) autopoint 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 getlimits 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 @@ -68,7 +68,7 @@ jobs: ## Install dependencies sudo apt-get update ## Check that build-gnu.sh works on the non SELinux system by installing libselinux only on lima - sudo apt-get install -y autopoint gperf gdb python3-pyinotify valgrind libexpect-perl libacl1-dev libattr1-dev libcap-dev attr quilt + sudo apt-get install -y gperf gdb python3-pyinotify valgrind libexpect-perl libacl1-dev libattr1-dev libcap-dev attr quilt curl http://launchpadlibrarian.net/831710181/automake_1.18.1-3_all.deb > automake-1.18.deb sudo dpkg -i --force-depends automake-1.18.deb - name: Add various locales @@ -245,7 +245,7 @@ jobs: - name: Install dependencies in VM run: | lima sudo dnf -y update - lima sudo dnf -y install git autoconf autopoint bison gperf gcc gdb jq libacl-devel libattr-devel libcap-devel libselinux-devel attr rustup clang-devel automake patch quilt + lima sudo dnf -y install git autoconf bison gperf gcc gdb jq libacl-devel libattr-devel libcap-devel libselinux-devel attr rustup clang-devel automake patch quilt lima rustup-init -y --default-toolchain stable - name: Copy the sources to VM run: | From 2be3404b47af8f945f987b45785742d6bb13964d Mon Sep 17 00:00:00 2001 From: xtqqczze <45661989+xtqqczze@users.noreply.github.com> Date: Thu, 1 Jan 2026 00:51:33 +0000 Subject: [PATCH 028/112] clippy: fix uninlined_format_args lint https://rust-lang.github.io/rust-clippy/master/index.html#uninlined_format_args --- src/uucore/src/lib/features/uptime.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/uucore/src/lib/features/uptime.rs b/src/uucore/src/lib/features/uptime.rs index 7e919b1ad75..cc4d976ae9f 100644 --- a/src/uucore/src/lib/features/uptime.rs +++ b/src/uucore/src/lib/features/uptime.rs @@ -501,8 +501,7 @@ mod tests { // (This is just a sanity check) assert!( uptime < 365 * 86400, - "Uptime seems unreasonably high: {} seconds", - uptime + "Uptime seems unreasonably high: {uptime} seconds" ); } @@ -518,9 +517,7 @@ mod tests { let diff = (uptime1 - uptime2).abs(); assert!( diff <= 1, - "Consecutive uptime calls should be consistent, got {} and {}", - uptime1, - uptime2 + "Consecutive uptime calls should be consistent, got {uptime1} and {uptime2}" ); } } From c9c063dc908e15dd0eba77c409927cc7747b5619 Mon Sep 17 00:00:00 2001 From: xtqqczze <45661989+xtqqczze@users.noreply.github.com> Date: Wed, 31 Dec 2025 21:16:04 +0000 Subject: [PATCH 029/112] uucore: switch to `BigDecimal::powi` implementation The implementation is identical. --- .../src/lib/features/parser/num_parser.rs | 73 +------------------ 1 file changed, 3 insertions(+), 70 deletions(-) diff --git a/src/uucore/src/lib/features/parser/num_parser.rs b/src/uucore/src/lib/features/parser/num_parser.rs index 178cd578fba..b23f51fb52e 100644 --- a/src/uucore/src/lib/features/parser/num_parser.rs +++ b/src/uucore/src/lib/features/parser/num_parser.rs @@ -7,10 +7,8 @@ // spell-checker:ignore powf copysign prec ilog inity infinit infs bigdecimal extendedbigdecimal biguint underflowed muls -use std::num::NonZeroU64; - use bigdecimal::{ - BigDecimal, Context, + BigDecimal, num_bigint::{BigInt, BigUint, Sign}, }; use num_traits::Signed; @@ -398,71 +396,6 @@ fn make_error(overflow: bool, negative: bool) -> ExtendedParserError -/// -/// TODO: Still pending discussion in , -/// we do lose a little bit of precision, and the last digits may not be correct. -/// Note: This has been copied from the latest revision in , -/// so it's using minimum Rust version of `bigdecimal-rs`. -fn pow_with_context(bd: &BigDecimal, exp: i64, ctx: &Context) -> BigDecimal { - if exp == 0 { - return 1.into(); - } - - // When performing a multiplication between 2 numbers, we may lose up to 2 digits - // of precision. - // "Proof": https://github.com/akubera/bigdecimal-rs/issues/147#issuecomment-2793431202 - const MARGIN_PER_MUL: u64 = 2; - // When doing many multiplication, we still introduce additional errors, add 1 more digit - // per 10 multiplications. - const MUL_PER_MARGIN_EXTRA: u64 = 10; - - fn trim_precision(bd: BigDecimal, ctx: &Context, margin: u64) -> BigDecimal { - let prec = ctx.precision().get() + margin; - if bd.digits() > prec { - bd.with_precision_round(NonZeroU64::new(prec).unwrap(), ctx.rounding_mode()) - } else { - bd - } - } - - // Count the number of multiplications we're going to perform, one per "1" binary digit - // in exp, and the number of times we can divide exp by 2. - let mut n = exp.unsigned_abs(); - // Note: 63 - n.leading_zeros() == n.ilog2, but that's only available in recent Rust versions. - let muls = (n.count_ones() + (63 - n.leading_zeros()) - 1) as u64; - // Note: div_ceil would be nice to use here, but only available in recent Rust versions. - // (see note above about minimum Rust version in use) - let margin_extra = (muls + MUL_PER_MARGIN_EXTRA / 2) / MUL_PER_MARGIN_EXTRA; - let mut margin = margin_extra + MARGIN_PER_MUL * muls; - - let mut bd_y: BigDecimal = 1.into(); - let mut bd_x = if exp >= 0 { - bd.clone() - } else { - bd.inverse_with_context(&ctx.with_precision( - NonZeroU64::new(ctx.precision().get() + margin + MARGIN_PER_MUL).unwrap(), - )) - }; - - while n > 1 { - if n % 2 == 1 { - bd_y = trim_precision(&bd_x * bd_y, ctx, margin); - margin -= MARGIN_PER_MUL; - n -= 1; - } - bd_x = trim_precision(bd_x.square(), ctx, margin); - margin -= MARGIN_PER_MUL; - n /= 2; - } - debug_assert_eq!(margin, margin_extra); - - trim_precision(bd_x * bd_y, ctx, 0) -} - /// Construct an [`ExtendedBigDecimal`] based on parsed data fn construct_extended_big_decimal( digits: BigUint, @@ -510,7 +443,7 @@ fn construct_extended_big_decimal( let bd = BigDecimal::from_bigint(signed_digits, 0) / BigDecimal::from_bigint(BigInt::from(16).pow(scale as u32), 0); - // pow_with_context "only" supports i64 values. Just overflow/underflow if the value provided + // powi "only" supports i64 values. Just overflow/underflow if the value provided // is > 2**64 or < 2**-64. let Some(exponent) = exponent.to_i64() else { return Err(make_error(exponent.is_positive(), negative)); @@ -520,7 +453,7 @@ fn construct_extended_big_decimal( let base: BigDecimal = 2.into(); // Note: We cannot overflow/underflow BigDecimal here, as we will not be able to reach the // maximum/minimum scale (i64 range). - let pow2 = pow_with_context(&base, exponent, &Context::default()); + let pow2 = base.powi(exponent); bd * pow2 } else { From a70edc291f6617ee82579b20ce838c9139ac9300 Mon Sep 17 00:00:00 2001 From: xtqqczze <45661989+xtqqczze@users.noreply.github.com> Date: Wed, 31 Dec 2025 20:54:48 +0000 Subject: [PATCH 030/112] Revert "df: disable clippy::assigning_clones on OpenBSD" This reverts commit 225a1052a78d96bf561edeb20379535e71d2d691. --- src/uu/df/src/filesystem.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/uu/df/src/filesystem.rs b/src/uu/df/src/filesystem.rs index 25743941db0..041f739596b 100644 --- a/src/uu/df/src/filesystem.rs +++ b/src/uu/df/src/filesystem.rs @@ -291,9 +291,7 @@ mod tests { } #[test] - // clippy::assigning_clones added with Rust 1.78 - // Rust version = 1.76 on OpenBSD stable/7.5 - #[cfg_attr(not(target_os = "openbsd"), allow(clippy::assigning_clones))] + #[allow(clippy::assigning_clones)] fn test_dev_name_match() { let tmp = tempfile::TempDir::new().expect("Failed to create temp dir"); let dev_name = std::fs::canonicalize(tmp.path()) From f1722e04fa80eb8650e9203d5dbc24246dfd0e2c Mon Sep 17 00:00:00 2001 From: xtqqczze <45661989+xtqqczze@users.noreply.github.com> Date: Wed, 31 Dec 2025 20:55:15 +0000 Subject: [PATCH 031/112] Revert "cp: disable clippy::assigning_clones on OpenBSD" This reverts commit 7a556a6e82d38749a92b60a986f7430e82e79282. --- src/uu/cp/src/cp.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index 1502a7ada72..1de71d36ae0 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -1279,9 +1279,7 @@ fn parse_path_args( }; if options.strip_trailing_slashes { - // clippy::assigning_clones added with Rust 1.78 - // Rust version = 1.76 on OpenBSD stable/7.5 - #[cfg_attr(not(target_os = "openbsd"), allow(clippy::assigning_clones))] + #[allow(clippy::assigning_clones)] for source in &mut paths { *source = source.components().as_path().to_owned(); } From 53448d36a77b50e94cb445adffafff46a6cf82ca Mon Sep 17 00:00:00 2001 From: xtqqczze <45661989+xtqqczze@users.noreply.github.com> Date: Wed, 31 Dec 2025 20:55:29 +0000 Subject: [PATCH 032/112] Revert "tail: disable clippy::assigning_clones on OpenBSD" This reverts commit 14258b12ad15442dafca91301e4f2e68a7884aee. --- src/uu/tail/src/follow/watch.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/uu/tail/src/follow/watch.rs b/src/uu/tail/src/follow/watch.rs index 95f38aabc67..b4b4d00acf4 100644 --- a/src/uu/tail/src/follow/watch.rs +++ b/src/uu/tail/src/follow/watch.rs @@ -47,9 +47,7 @@ impl WatcherRx { Tested for notify::InotifyWatcher and for notify::PollWatcher. */ if let Some(parent) = path.parent() { - // clippy::assigning_clones added with Rust 1.78 - // Rust version = 1.76 on OpenBSD stable/7.5 - #[cfg_attr(not(target_os = "openbsd"), allow(clippy::assigning_clones))] + #[allow(clippy::assigning_clones)] if parent.is_dir() { path = parent.to_owned(); } else { From 08e4910acd86eaa2f28e88af7ae9d636f2df59c5 Mon Sep 17 00:00:00 2001 From: Martin Paulsen <43757366+georgepaulsen@users.noreply.github.com> Date: Thu, 1 Jan 2026 04:58:18 -0500 Subject: [PATCH 033/112] test: fixing unary operators that are getting parsed as argument instead of string literal (#9951) * Check for three-string comparison in test * Add regression tests for unary operator in three-arg form --- src/uu/test/src/parser.rs | 11 ++++++++++- tests/by-util/test_test.rs | 7 +++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/uu/test/src/parser.rs b/src/uu/test/src/parser.rs index 167bf77023a..c1c06e4c581 100644 --- a/src/uu/test/src/parser.rs +++ b/src/uu/test/src/parser.rs @@ -188,7 +188,16 @@ impl Parser { match symbol { Symbol::LParen => self.lparen()?, Symbol::Bang => self.bang()?, - Symbol::UnaryOp(_) => self.uop(symbol), + Symbol::UnaryOp(_) => { + // Three-argument string comparison: `-f = a` means "-f" = "a", not file test + let is_string_cmp = matches!(self.peek(), Symbol::Op(Operator::String(_))) + && !matches!(Symbol::new(self.tokens.clone().nth(1)), Symbol::None); + if is_string_cmp { + self.literal(symbol.into_literal())?; + } else { + self.uop(symbol); + } + } Symbol::None => self.stack.push(symbol), literal => self.literal(literal)?, } diff --git a/tests/by-util/test_test.rs b/tests/by-util/test_test.rs index 4b5460cfd4a..21ea1893e99 100644 --- a/tests/by-util/test_test.rs +++ b/tests/by-util/test_test.rs @@ -1027,3 +1027,10 @@ fn test_string_lt_gt_operator() { .fails_with_code(1) .no_output(); } + +#[test] +fn test_unary_op_as_literal_in_three_arg_form() { + // `-f = a` is string comparison "-f" = "a", not file test + new_ucmd!().args(&["-f", "=", "a"]).fails_with_code(1); + new_ucmd!().args(&["-f", "=", "a", "-o", "b"]).succeeds(); +} From b5fb61507c63ff343e4e8c6bc6d990c9a4234760 Mon Sep 17 00:00:00 2001 From: Yuankun Zhang Date: Thu, 1 Jan 2026 18:01:11 +0800 Subject: [PATCH 034/112] mv: support moving folder containing symlinks to different filesystem (#8605) Co-authored-by: Sylvestre Ledru --- src/uu/mv/src/mv.rs | 29 +++++++++++++++------- tests/by-util/test_mv.rs | 52 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 9 deletions(-) diff --git a/src/uu/mv/src/mv.rs b/src/uu/mv/src/mv.rs index a43b92eb850..aa34a6294ae 100644 --- a/src/uu/mv/src/mv.rs +++ b/src/uu/mv/src/mv.rs @@ -1099,7 +1099,13 @@ fn copy_dir_contents_recursive( } #[cfg(not(unix))] { - fs::copy(&from_path, &to_path)?; + if from_path.is_symlink() { + // Copy a symlink file (no-follow). + rename_symlink_fallback(&from_path, &to_path)?; + } else { + // Copy a regular file. + fs::copy(&from_path, &to_path)?; + } } // Print verbose message for file @@ -1142,14 +1148,19 @@ fn copy_file_with_hardlinks_helper( return Ok(()); } - // Regular file copy - #[cfg(all(unix, not(any(target_os = "macos", target_os = "redox"))))] - { - fs::copy(from, to).and_then(|_| fsxattr::copy_xattrs(&from, &to))?; - } - #[cfg(any(target_os = "macos", target_os = "redox"))] - { - fs::copy(from, to)?; + if from.is_symlink() { + // Copy a symlink file (no-follow). + rename_symlink_fallback(from, to)?; + } else { + // Copy a regular file. + #[cfg(all(unix, not(any(target_os = "macos", target_os = "redox"))))] + { + fs::copy(from, to).and_then(|_| fsxattr::copy_xattrs(&from, &to))?; + } + #[cfg(any(target_os = "macos", target_os = "redox"))] + { + fs::copy(from, to)?; + } } Ok(()) diff --git a/tests/by-util/test_mv.rs b/tests/by-util/test_mv.rs index 7e22d930b49..3c69d65a78d 100644 --- a/tests/by-util/test_mv.rs +++ b/tests/by-util/test_mv.rs @@ -623,6 +623,58 @@ fn test_mv_symlink_into_target() { ucmd.arg("dir-link").arg("dir").succeeds(); } +#[cfg(all(unix, not(target_os = "android")))] +#[ignore = "requires sudo"] +#[test] +fn test_mv_broken_symlink_to_another_fs() { + let scene = TestScenario::new(util_name!()); + + scene.fixtures.mkdir("foo"); + + let output = scene + .cmd("sudo") + .env("PATH", env!("PATH")) + .args(&["-E", "--non-interactive", "ls"]) + .run(); + println!("test output: {output:?}"); + + let mount = scene + .cmd("sudo") + .env("PATH", env!("PATH")) + .args(&[ + "-E", + "--non-interactive", + "mount", + "none", + "-t", + "tmpfs", + "foo", + ]) + .run(); + + if !mount.succeeded() { + print!("Test skipped; requires root user"); + return; + } + + scene.fixtures.mkdir("bar"); + scene.fixtures.symlink_file("nonexistent", "bar/baz"); + + scene + .ucmd() + .arg("bar") + .arg("foo") + .succeeds() + .no_stderr() + .no_stdout(); + + scene + .cmd("sudo") + .env("PATH", env!("PATH")) + .args(&["-E", "--non-interactive", "umount", "foo"]) + .succeeds(); +} + #[test] #[cfg(all(unix, not(target_os = "android")))] fn test_mv_hardlink_to_symlink() { From 27bc7458194357955ae1817035aafef435039c60 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Thu, 1 Jan 2026 17:26:22 +0900 Subject: [PATCH 035/112] GnuTests.yml: Drop git from VM --- .github/workflows/GnuTests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/GnuTests.yml b/.github/workflows/GnuTests.yml index d50074b901c..0cac5365722 100644 --- a/.github/workflows/GnuTests.yml +++ b/.github/workflows/GnuTests.yml @@ -245,7 +245,7 @@ jobs: - name: Install dependencies in VM run: | lima sudo dnf -y update - lima sudo dnf -y install git autoconf bison gperf gcc gdb jq libacl-devel libattr-devel libcap-devel libselinux-devel attr rustup clang-devel automake patch quilt + lima sudo dnf -y install autoconf bison gperf gcc gdb jq libacl-devel libattr-devel libcap-devel libselinux-devel attr rustup clang-devel automake patch quilt lima rustup-init -y --default-toolchain stable - name: Copy the sources to VM run: | From 92832f2bf344ed92f889cb7e9adbae482099caa0 Mon Sep 17 00:00:00 2001 From: cerdelen <95369756+cerdelen@users.noreply.github.com> Date: Thu, 1 Jan 2026 11:41:05 +0100 Subject: [PATCH 036/112] chmod: fix error handling if multiple files are handled (#9793) * chmod: fix error handling if multiple files are handled * chmod: add regression test for correct exit codes * chmod: fix test expected error msg --------- Co-authored-by: Sylvestre Ledru --- src/uu/chmod/src/chmod.rs | 2 +- tests/by-util/test_chmod.rs | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/uu/chmod/src/chmod.rs b/src/uu/chmod/src/chmod.rs index 24566272b23..b7e0f3fd965 100644 --- a/src/uu/chmod/src/chmod.rs +++ b/src/uu/chmod/src/chmod.rs @@ -411,7 +411,7 @@ impl Chmoder { return Err(ChmodError::PreserveRoot("/".into()).into()); } if self.recursive { - r = self.walk_dir_with_context(file, true); + r = self.walk_dir_with_context(file, true).and(r); } else { r = self.chmod_file(file).and(r); } diff --git a/tests/by-util/test_chmod.rs b/tests/by-util/test_chmod.rs index 446cdd6d39e..5e340732832 100644 --- a/tests/by-util/test_chmod.rs +++ b/tests/by-util/test_chmod.rs @@ -375,6 +375,38 @@ fn test_permission_denied() { .stderr_is("chmod: cannot access 'd/no-x/y': Permission denied\n"); } +#[test] +#[allow(clippy::unreadable_literal)] +fn test_chmod_recursive_correct_exit_code() { + let (at, mut ucmd) = at_and_ucmd!(); + + // create 3 folders to test on + at.mkdir("a"); + at.mkdir("a/b"); + at.mkdir("z"); + + // remove read permissions for folder a so the chmod command for a/b fails + let mut perms = at.metadata("a").permissions(); + perms.set_mode(0o000); + set_permissions(at.plus_as_string("a"), perms).unwrap(); + + #[cfg(not(target_os = "linux"))] + let err_msg = "chmod: Permission denied\n"; + #[cfg(target_os = "linux")] + let err_msg = "chmod: cannot access 'a': Permission denied\n"; + + // order of command is a, a/b then c + // command is expected to fail and not just take the last exit code + ucmd.arg("-R") + .arg("--verbose") + .arg("a+w") + .arg("a") + .arg("z") + .umask(0) + .fails() + .stderr_is(err_msg); +} + #[test] #[allow(clippy::unreadable_literal)] fn test_chmod_recursive() { From 5c45d87439b5a72de4193b56c9083228aaae5620 Mon Sep 17 00:00:00 2001 From: Saathwik Dasari Date: Thu, 1 Jan 2026 17:06:58 +0530 Subject: [PATCH 037/112] pr: allow character, block, and fifo devices as input (#9946) --- src/uu/pr/src/pr.rs | 31 +++++++++++++++++++------------ tests/by-util/test_pr.rs | 6 ++++++ 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/uu/pr/src/pr.rs b/src/uu/pr/src/pr.rs index f5c5662aa19..19e9f2a0c89 100644 --- a/src/uu/pr/src/pr.rs +++ b/src/uu/pr/src/pr.rs @@ -757,22 +757,29 @@ fn open(path: &str) -> Result, PrError> { |i| { let path_string = path.to_string(); match i.file_type() { - #[cfg(unix)] - ft if ft.is_block_device() => Err(PrError::UnknownFiletype { file: path_string }), - #[cfg(unix)] - ft if ft.is_char_device() => Err(PrError::UnknownFiletype { file: path_string }), - #[cfg(unix)] - ft if ft.is_fifo() => Err(PrError::UnknownFiletype { file: path_string }), #[cfg(unix)] ft if ft.is_socket() => Err(PrError::IsSocket { file: path_string }), ft if ft.is_dir() => Err(PrError::IsDirectory { file: path_string }), - ft if ft.is_file() || ft.is_symlink() => { - Ok(Box::new(File::open(path).map_err(|e| PrError::Input { - source: e, - file: path.to_string(), - })?) as Box) + + ft => { + #[allow(unused_mut)] + let mut is_valid = ft.is_file() || ft.is_symlink(); + + #[cfg(unix)] + { + is_valid = + is_valid || ft.is_char_device() || ft.is_block_device() || ft.is_fifo(); + } + + if is_valid { + Ok(Box::new(File::open(path).map_err(|e| PrError::Input { + source: e, + file: path.to_string(), + })?) as Box) + } else { + Err(PrError::UnknownFiletype { file: path_string }) + } } - _ => Err(PrError::UnknownFiletype { file: path_string }), } }, ) diff --git a/tests/by-util/test_pr.rs b/tests/by-util/test_pr.rs index 1fa91dab2e9..0bb161fb8a8 100644 --- a/tests/by-util/test_pr.rs +++ b/tests/by-util/test_pr.rs @@ -610,3 +610,9 @@ fn test_help() { fn test_version() { new_ucmd!().arg("--version").succeeds(); } + +#[cfg(unix)] +#[test] +fn test_pr_char_device_dev_null() { + new_ucmd!().arg("/dev/null").succeeds(); +} From 5cd1d6a6f79abe1072b996a73d0f9dfe321a5d66 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Thu, 1 Jan 2026 20:37:37 +0900 Subject: [PATCH 038/112] build-gnu.sh: Skip make at SELinux tests (#9970) --- util/build-gnu.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/util/build-gnu.sh b/util/build-gnu.sh index 734cde88cbd..f1cba68ad4c 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -139,6 +139,7 @@ else # Skip make if possible # Use GNU nproc for *BSD and macOS NPROC="$(command -v nproc||command -v gnproc)" + test "${SELINUX_ENABLED}" = 1 && touch src/getlimits # SELinux tests does not use it test -f src/getlimits || "${MAKE}" -j "$("${NPROC}")" cp -f src/getlimits "${UU_BUILD_DIR}" From d08481a2a249747581673a53b7bac7a44533221a Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Thu, 1 Jan 2026 20:37:59 +0900 Subject: [PATCH 039/112] build-gnu.sh: Drop a variable & cleanup (#9953) --- util/build-gnu.sh | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/util/build-gnu.sh b/util/build-gnu.sh index f1cba68ad4c..7e92396bfc3 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -22,8 +22,8 @@ REPO_main_dir="$(dirname -- "${ME_dir}")" : ${PROFILE:=debug} # default profile -export PROFILE -CARGO_FEATURE_FLAGS="" +export PROFILE # tell to make +unset CARGOFLAGS ### * config (from environment with fallback defaults); note: GNU is expected to be a sibling repo directory @@ -63,15 +63,15 @@ echo "UU_BUILD_DIR='${UU_BUILD_DIR}'" cd "${path_UUTILS}" && echo "[ pwd:'${PWD}' ]" export SELINUX_ENABLED # Run this script with=1 for testing SELinux -[ "${SELINUX_ENABLED}" = 1 ] && CARGO_FEATURE_FLAGS="${CARGO_FEATURE_FLAGS} selinux" +[ "${SELINUX_ENABLED}" = 1 ] && CARGOFLAGS="${CARGOFLAGS} selinux" # Trim leading whitespace from feature flags -CARGO_FEATURE_FLAGS="$(echo "${CARGO_FEATURE_FLAGS}" | sed -e 's/^[[:space:]]*//')" +CARGOFLAGS="$(echo "${CARGOFLAGS}" | sed -e 's/^[[:space:]]*//')" # If we have feature flags, format them correctly for cargo -if [ ! -z "${CARGO_FEATURE_FLAGS}" ]; then - CARGO_FEATURE_FLAGS="--features ${CARGO_FEATURE_FLAGS}" - echo "Building with cargo flags: ${CARGO_FEATURE_FLAGS}" +if [ ! -z "${CARGOFLAGS}" ]; then + CARGOFLAGS="--features ${CARGOFLAGS}" + echo "Building with cargo flags: ${CARGOFLAGS}" fi # Set up quilt for patch management @@ -87,15 +87,16 @@ else fi cd - +export CARGOFLAGS # tell to make # bug: seq with MULTICALL=y breaks env-signal-handler.sh - "${MAKE}" UTILS="install seq" PROFILE="${PROFILE}" CARGOFLAGS="${CARGO_FEATURE_FLAGS}" + "${MAKE}" UTILS="install seq" ln -vf "${UU_BUILD_DIR}/install" "${UU_BUILD_DIR}/ginstall" # The GNU tests use renamed install to ginstall if [ "${SELINUX_ENABLED}" = 1 ];then # Build few utils for SELinux for faster build. MULTICALL=y fails... - "${MAKE}" UTILS="cat chcon cp cut echo env groups id ln ls mkdir mkfifo mknod mktemp mv printf rm rmdir runcon stat test touch tr true uname wc whoami" PROFILE="${PROFILE}" CARGOFLAGS="${CARGO_FEATURE_FLAGS}" + "${MAKE}" UTILS="cat chcon cp cut echo env groups id ln ls mkdir mkfifo mknod mktemp mv printf rm rmdir runcon stat test touch tr true uname wc whoami" else # Use MULTICALL=y for faster build - "${MAKE}" MULTICALL=y SKIP_UTILS="install more seq" PROFILE="${PROFILE}" CARGOFLAGS="${CARGO_FEATURE_FLAGS}" + "${MAKE}" MULTICALL=y SKIP_UTILS="install more seq" for binary in $("${UU_BUILD_DIR}"/coreutils --list) do ln -vf "${UU_BUILD_DIR}/coreutils" "${UU_BUILD_DIR}/${binary}" done @@ -109,9 +110,7 @@ cd "${path_GNU}" && echo "[ pwd:'${PWD}' ]" # Note that some test (e.g. runcon/runcon-compute.sh) incorrectly passes by this for binary in $(./build-aux/gen-lists-of-programs.sh --list-progs); do bin_path="${UU_BUILD_DIR}/${binary}" - test -f "${bin_path}" || { - cp -v /usr/bin/false "${bin_path}" - } + test -f "${bin_path}" || cp -v /usr/bin/false "${bin_path}" done # Always update the PATH to test the uutils coreutils instead of the GNU coreutils From b5a60a99d44bea655eb535d5188046772dc4ae1e Mon Sep 17 00:00:00 2001 From: Jane Illarionova Date: Thu, 1 Jan 2026 06:38:26 -0500 Subject: [PATCH 040/112] Unexpand: use byte count for multibyte characters for column width when using -a flag (#9949) * Remove Unicode width calculation for characters * Add test for unexpand with multibyte UTF-8 input --- src/uu/unexpand/src/unexpand.rs | 7 +------ tests/by-util/test_unexpand.rs | 12 ++++++++++++ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/uu/unexpand/src/unexpand.rs b/src/uu/unexpand/src/unexpand.rs index b3990ac596f..896318484dd 100644 --- a/src/uu/unexpand/src/unexpand.rs +++ b/src/uu/unexpand/src/unexpand.rs @@ -13,7 +13,6 @@ use std::num::IntErrorKind; use std::path::Path; use std::str::from_utf8; use thiserror::Error; -use unicode_width::UnicodeWidthChar; use uucore::display::Quotable; use uucore::error::{FromIo, UError, UResult, USimpleError}; use uucore::translate; @@ -279,11 +278,7 @@ fn next_char_info(uflag: bool, buf: &[u8], byte: usize) -> (CharType, usize, usi Some(' ') => (CharType::Space, 0, 1), Some('\t') => (CharType::Tab, 0, 1), Some('\x08') => (CharType::Backspace, 0, 1), - Some(c) => ( - CharType::Other, - UnicodeWidthChar::width(c).unwrap_or(0), - nbytes, - ), + Some(_) => (CharType::Other, nbytes, nbytes), None => { // invalid char snuck past the utf8_validation_iterator somehow??? (CharType::Other, 1, 1) diff --git a/tests/by-util/test_unexpand.rs b/tests/by-util/test_unexpand.rs index 0f2a6d464fe..0720dabb043 100644 --- a/tests/by-util/test_unexpand.rs +++ b/tests/by-util/test_unexpand.rs @@ -295,3 +295,15 @@ fn test_non_utf8_filename() { ucmd.arg(&filename).succeeds().stdout_is("\ta\n"); } + +#[test] +fn unexpand_multibyte_utf8_gnu_compat() { + // Verifies GNU-compatible behavior: column position uses byte count, not display width + // "1ΔΔΔ5" is 8 bytes (1 + 2*3 + 1), already at tab stop 8 + // So 3 spaces should NOT convert to tab (would need 8 more to reach tab stop 16) + new_ucmd!() + .args(&["-a"]) + .pipe_in("1ΔΔΔ5 99999\n") + .succeeds() + .stdout_is("1ΔΔΔ5 99999\n"); +} From 82b80a1a1e5032ab803ed6b37b95ad113662dc4f Mon Sep 17 00:00:00 2001 From: Dylan Skelly <43255611+Dylans123@users.noreply.github.com> Date: Thu, 1 Jan 2026 09:04:47 -0500 Subject: [PATCH 041/112] cp: symlink flags fixing conflicting flag logic to use last flag (#9960) * Adding overrides_with_all for symlink flags for ordering * Adding regression tests for symlink flag ordering --- src/uu/cp/src/cp.rs | 32 +++++++++++++++++++++++++++-- tests/by-util/test_cp.rs | 44 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index 1de71d36ae0..3048f38b711 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -689,7 +689,12 @@ pub fn uu_app() -> Command { Arg::new(options::NO_DEREFERENCE) .short('P') .long(options::NO_DEREFERENCE) - .overrides_with(options::DEREFERENCE) + .overrides_with_all([ + options::DEREFERENCE, + options::CLI_SYMBOLIC_LINKS, + options::ARCHIVE, + options::NO_DEREFERENCE_PRESERVE_LINKS, + ]) // -d sets this option .help(translate!("cp-help-no-dereference")) .action(ArgAction::SetTrue), @@ -698,13 +703,24 @@ pub fn uu_app() -> Command { Arg::new(options::DEREFERENCE) .short('L') .long(options::DEREFERENCE) - .overrides_with(options::NO_DEREFERENCE) + .overrides_with_all([ + options::NO_DEREFERENCE, + options::CLI_SYMBOLIC_LINKS, + options::ARCHIVE, + options::NO_DEREFERENCE_PRESERVE_LINKS, + ]) .help(translate!("cp-help-dereference")) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::CLI_SYMBOLIC_LINKS) .short('H') + .overrides_with_all([ + options::DEREFERENCE, + options::NO_DEREFERENCE, + options::ARCHIVE, + options::NO_DEREFERENCE_PRESERVE_LINKS, + ]) .help(translate!("cp-help-cli-symbolic-links")) .action(ArgAction::SetTrue), ) @@ -712,12 +728,24 @@ pub fn uu_app() -> Command { Arg::new(options::ARCHIVE) .short('a') .long(options::ARCHIVE) + .overrides_with_all([ + options::DEREFERENCE, + options::NO_DEREFERENCE, + options::CLI_SYMBOLIC_LINKS, + options::NO_DEREFERENCE_PRESERVE_LINKS, + ]) .help(translate!("cp-help-archive")) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::NO_DEREFERENCE_PRESERVE_LINKS) .short('d') + .overrides_with_all([ + options::DEREFERENCE, + options::NO_DEREFERENCE, + options::CLI_SYMBOLIC_LINKS, + options::ARCHIVE, + ]) .help(translate!("cp-help-no-dereference-preserve-links")) .action(ArgAction::SetTrue), ) diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index 2563e533ae4..5f4a44c4aff 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -7400,3 +7400,47 @@ fn test_cp_recurse_verbose_output_with_symlink_already_exists() { .no_stderr() .stdout_is(output); } + +#[test] +#[cfg(unix)] +fn test_cp_hlp_flag_ordering() { + // GNU cp: "If more than one of -H, -L, and -P is specified, only the final one takes effect" + let (at, mut ucmd) = at_and_ucmd!(); + at.touch("file.txt"); + at.symlink_file("file.txt", "symlink"); + + // -HP: P wins, copy symlink as symlink + ucmd.args(&["-HP", "symlink", "dest_hp"]).succeeds(); + assert!(at.is_symlink("dest_hp")); + + // -PH: H wins, copy target file + let (at, mut ucmd) = at_and_ucmd!(); + at.touch("file.txt"); + at.symlink_file("file.txt", "symlink"); + ucmd.args(&["-PH", "symlink", "dest_ph"]).succeeds(); + assert!(!at.is_symlink("dest_ph")); + assert!(at.file_exists("dest_ph")); +} + +#[test] +#[cfg(unix)] +fn test_cp_archive_deref_flag_ordering() { + // (flags, expect_symlink): last flag wins; a/d imply -P, H/L dereference + for (flags, expect_symlink) in [ + ("-Ha", true), + ("-aH", false), + ("-Hd", true), + ("-dH", false), + ("-La", true), + ("-aL", false), + ("-Ld", true), + ("-dL", false), + ] { + let (at, mut ucmd) = at_and_ucmd!(); + at.touch("file.txt"); + at.symlink_file("file.txt", "symlink"); + let dest = format!("dest{flags}"); + ucmd.args(&[flags, "symlink", &dest]).succeeds(); + assert_eq!(at.is_symlink(&dest), expect_symlink, "failed for {flags}"); + } +} From 1a5a140be723195c57555c60e2352157e6749bd5 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Fri, 2 Jan 2026 01:04:53 +0900 Subject: [PATCH 042/112] hashsum: Drop benches as covered by cksum benches (#9842) Co-authored-by: oech3 <> Co-authored-by: Sylvestre Ledru --- .github/workflows/benchmarks.yml | 1 - src/uu/hashsum/BENCHMARKING.md | 11 -- src/uu/hashsum/Cargo.toml | 4 - src/uu/hashsum/benches/hashsum_bench.rs | 138 ------------------------ 4 files changed, 154 deletions(-) delete mode 100644 src/uu/hashsum/BENCHMARKING.md delete mode 100644 src/uu/hashsum/benches/hashsum_bench.rs diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 1ffce535dae..76fe09b7a04 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -31,7 +31,6 @@ jobs: - { package: uu_du } - { package: uu_expand } - { package: uu_fold } - - { package: uu_hashsum } - { package: uu_ls } - { package: uu_mv } - { package: uu_nl } diff --git a/src/uu/hashsum/BENCHMARKING.md b/src/uu/hashsum/BENCHMARKING.md deleted file mode 100644 index 9508cae1b66..00000000000 --- a/src/uu/hashsum/BENCHMARKING.md +++ /dev/null @@ -1,11 +0,0 @@ -# Benchmarking hashsum - -## To bench blake2 - -Taken from: - -With a large file: - -```shell -hyperfine "./target/release/coreutils hashsum --b2sum large-file" "b2sum large-file" -``` diff --git a/src/uu/hashsum/Cargo.toml b/src/uu/hashsum/Cargo.toml index ec382870bdc..f77c2c52d84 100644 --- a/src/uu/hashsum/Cargo.toml +++ b/src/uu/hashsum/Cargo.toml @@ -30,7 +30,3 @@ path = "src/main.rs" divan = { workspace = true } tempfile = { workspace = true } uucore = { workspace = true, features = ["benchmark"] } - -[[bench]] -name = "hashsum_bench" -harness = false diff --git a/src/uu/hashsum/benches/hashsum_bench.rs b/src/uu/hashsum/benches/hashsum_bench.rs deleted file mode 100644 index 27572c560b4..00000000000 --- a/src/uu/hashsum/benches/hashsum_bench.rs +++ /dev/null @@ -1,138 +0,0 @@ -// 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 divan::{Bencher, black_box}; -use std::io::Write; -use tempfile::NamedTempFile; -use uu_hashsum::uumain; -use uucore::benchmark::{run_util_function, setup_test_file, text_data}; - -/// Benchmark MD5 hashing -#[divan::bench] -fn hashsum_md5(bencher: Bencher) { - let data = text_data::generate_by_size(10, 80); - let file_path = setup_test_file(&data); - - bencher.bench(|| { - black_box(run_util_function( - uumain, - &["--md5", file_path.to_str().unwrap()], - )); - }); -} - -/// Benchmark SHA1 hashing -#[divan::bench] -fn hashsum_sha1(bencher: Bencher) { - let data = text_data::generate_by_size(10, 80); - let file_path = setup_test_file(&data); - - bencher.bench(|| { - black_box(run_util_function( - uumain, - &["--sha1", file_path.to_str().unwrap()], - )); - }); -} - -/// Benchmark SHA256 hashing -#[divan::bench] -fn hashsum_sha256(bencher: Bencher) { - let data = text_data::generate_by_size(10, 80); - let file_path = setup_test_file(&data); - - bencher.bench(|| { - black_box(run_util_function( - uumain, - &["--sha256", file_path.to_str().unwrap()], - )); - }); -} - -/// Benchmark SHA512 hashing -#[divan::bench] -fn hashsum_sha512(bencher: Bencher) { - let data = text_data::generate_by_size(10, 80); - let file_path = setup_test_file(&data); - - bencher.bench(|| { - black_box(run_util_function( - uumain, - &["--sha512", file_path.to_str().unwrap()], - )); - }); -} - -/// Benchmark MD5 checksum verification -#[divan::bench] -fn hashsum_md5_check(bencher: Bencher) { - bencher - .with_inputs(|| { - // Create test file - let data = text_data::generate_by_size(10, 80); - let test_file = setup_test_file(&data); - - // Create checksum file - keep it alive by returning it - let checksum_file = NamedTempFile::new().unwrap(); - let checksum_path = checksum_file.path().to_str().unwrap().to_string(); - - // Write checksum content - { - let mut file = std::fs::File::create(&checksum_path).unwrap(); - writeln!( - file, - "d41d8cd98f00b204e9800998ecf8427e {}", - test_file.to_str().unwrap() - ) - .unwrap(); - } - - (checksum_file, checksum_path) - }) - .bench_values(|(_checksum_file, checksum_path)| { - black_box(run_util_function( - uumain, - &["--md5", "--check", &checksum_path], - )); - }); -} - -/// Benchmark SHA256 checksum verification -#[divan::bench] -fn hashsum_sha256_check(bencher: Bencher) { - bencher - .with_inputs(|| { - // Create test file - let data = text_data::generate_by_size(10, 80); - let test_file = setup_test_file(&data); - - // Create checksum file - keep it alive by returning it - let checksum_file = NamedTempFile::new().unwrap(); - let checksum_path = checksum_file.path().to_str().unwrap().to_string(); - - // Write checksum content - { - let mut file = std::fs::File::create(&checksum_path).unwrap(); - writeln!( - file, - "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 {}", - test_file.to_str().unwrap() - ) - .unwrap(); - } - - (checksum_file, checksum_path) - }) - .bench_values(|(_checksum_file, checksum_path)| { - black_box(run_util_function( - uumain, - &["--sha256", "--check", &checksum_path], - )); - }); -} - -fn main() { - divan::main(); -} From a9b7330eef1b58040c9f85ee4049eadea9f93942 Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Thu, 1 Jan 2026 16:37:34 +0100 Subject: [PATCH 043/112] Bump signal-hook from 0.3.18 to 0.4.1 --- Cargo.lock | 16 +++++++++++++--- Cargo.toml | 2 +- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0d84035b97d..bf51e63c144 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -757,7 +757,7 @@ dependencies = [ "mio", "parking_lot", "rustix", - "signal-hook", + "signal-hook 0.3.18", "signal-hook-mio", "winapi", ] @@ -2607,6 +2607,16 @@ dependencies = [ "signal-hook-registry", ] +[[package]] +name = "signal-hook" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a37d01603c37b5466f808de79f845c7116049b0579adb70a6b7d47c1fa3a952" +dependencies = [ + "libc", + "signal-hook-registry", +] + [[package]] name = "signal-hook-mio" version = "0.2.5" @@ -2615,7 +2625,7 @@ checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" dependencies = [ "libc", "mio", - "signal-hook", + "signal-hook 0.3.18", ] [[package]] @@ -3206,7 +3216,7 @@ dependencies = [ "gcd", "libc", "nix", - "signal-hook", + "signal-hook 0.4.1", "tempfile", "thiserror 2.0.17", "uucore", diff --git a/Cargo.toml b/Cargo.toml index d6737d16d08..80af38b85c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -369,7 +369,7 @@ same-file = "1.0.6" self_cell = "1.0.4" # FIXME we use the exact version because the new 0.5.3 requires an MSRV of 1.88 selinux = "=0.5.2" -signal-hook = "0.3.17" +signal-hook = "0.4.1" tempfile = "3.15.0" terminal_size = "0.4.0" textwrap = { version = "0.16.1", features = ["terminal_size"] } From e05d925143503d168ff513869e182c1524468462 Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Thu, 1 Jan 2026 16:39:51 +0100 Subject: [PATCH 044/112] deny.toml: add signal-hook to skip list --- deny.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/deny.toml b/deny.toml index 662474b65cd..eb0e0230052 100644 --- a/deny.toml +++ b/deny.toml @@ -107,6 +107,8 @@ skip = [ { name = "zerocopy-derive", version = "0.7.35" }, # rustix { name = "linux-raw-sys", version = "0.11.0" }, + # crossterm + { name = "signal-hook", version = "0.3.18" }, ] # spell-checker: enable From 051d180181c5b76f2e5af8490a719f11c0db9054 Mon Sep 17 00:00:00 2001 From: cerdelen <95369756+cerdelen@users.noreply.github.com> Date: Thu, 1 Jan 2026 21:37:39 +0100 Subject: [PATCH 045/112] Merge pull request #9785 from cerdelen/fix_date_military_parsing Fix military date parsing not adjusting date --- src/uu/date/src/date.rs | 70 ++++++++++++++++++++++++++++++++------ tests/by-util/test_date.rs | 40 ++++++++++++++++++++++ 2 files changed, 99 insertions(+), 11 deletions(-) diff --git a/src/uu/date/src/date.rs b/src/uu/date/src/date.rs index c72b1c3048b..d6623e5a654 100644 --- a/src/uu/date/src/date.rs +++ b/src/uu/date/src/date.rs @@ -116,6 +116,20 @@ impl From<&str> for Rfc3339Format { } } +/// Indicates whether parsing a military timezone causes the date to remain the same, roll back to the previous day, or +/// advance to the next day. +/// This can occur when applying a military timezone with an optional hour offset crosses midnight +/// in either direction. +#[derive(PartialEq, Debug)] +enum DayDelta { + /// The date does not change + Same, + /// The date rolls back to the previous day. + Previous, + /// The date advances to the next day. + Next, +} + /// Parse military timezone with optional hour offset. /// Pattern: single letter (a-z except j) optionally followed by 1-2 digits. /// Returns Some(total_hours_in_utc) or None if pattern doesn't match. @@ -128,7 +142,7 @@ impl From<&str> for Rfc3339Format { /// /// The hour offset from digits is added to the base military timezone offset. /// Examples: "m" -> 12 (noon UTC), "m9" -> 21 (9pm UTC), "a5" -> 4 (4am UTC next day) -fn parse_military_timezone_with_offset(s: &str) -> Option { +fn parse_military_timezone_with_offset(s: &str) -> Option<(i32, DayDelta)> { if s.is_empty() || s.len() > 3 { return None; } @@ -160,11 +174,17 @@ fn parse_military_timezone_with_offset(s: &str) -> Option { _ => return None, }; + let day_delta = match additional_hours - tz_offset { + h if h < 0 => DayDelta::Previous, + h if h >= 24 => DayDelta::Next, + _ => DayDelta::Same, + }; + // Calculate total hours: midnight (0) + tz_offset + additional_hours // Midnight in timezone X converted to UTC - let total_hours = (0 - tz_offset + additional_hours).rem_euclid(24); + let hours_from_midnight = (0 - tz_offset + additional_hours).rem_euclid(24); - Some(total_hours) + Some((hours_from_midnight, day_delta)) } #[uucore::main] @@ -306,11 +326,24 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { format!("{date_part} 00:00 {offset}") }; parse_date(composed) - } else if let Some(total_hours) = military_tz_with_offset { + } else if let Some((total_hours, day_delta)) = military_tz_with_offset { // Military timezone with optional hour offset // Convert to UTC time: midnight + military_tz_offset + additional_hours - let date_part = - strtime::format("%F", &now).unwrap_or_else(|_| String::from("1970-01-01")); + + // When calculating a military timezone with an optional hour offset, midnight may + // be crossed in either direction. `day_delta` indicates whether the date remains + // the same, moves to the previous day, or advances to the next day. + // Changing day can result in error, this closure will help handle these errors + // gracefully. + let format_date_with_epoch_fallback = |date: Result| -> String { + date.and_then(|d| strtime::format("%F", &d)) + .unwrap_or_else(|_| String::from("1970-01-01")) + }; + let date_part = match day_delta { + DayDelta::Same => format_date_with_epoch_fallback(Ok(now)), + DayDelta::Next => format_date_with_epoch_fallback(now.tomorrow()), + DayDelta::Previous => format_date_with_epoch_fallback(now.yesterday()), + }; let composed = format!("{date_part} {total_hours:02}:00:00 +00:00"); parse_date(composed) } else if is_pure_digits { @@ -817,11 +850,26 @@ mod tests { #[test] fn test_parse_military_timezone_with_offset() { // Valid cases: letter only, letter + digit, uppercase - assert_eq!(parse_military_timezone_with_offset("m"), Some(12)); // UTC+12 -> 12:00 UTC - assert_eq!(parse_military_timezone_with_offset("m9"), Some(21)); // 12 + 9 = 21 - assert_eq!(parse_military_timezone_with_offset("a5"), Some(4)); // 23 + 5 = 28 % 24 = 4 - assert_eq!(parse_military_timezone_with_offset("z"), Some(0)); // UTC+0 -> 00:00 UTC - assert_eq!(parse_military_timezone_with_offset("M9"), Some(21)); // Uppercase works + assert_eq!( + parse_military_timezone_with_offset("m"), + Some((12, DayDelta::Previous)) + ); // UTC+12 -> 12:00 UTC + assert_eq!( + parse_military_timezone_with_offset("m9"), + Some((21, DayDelta::Previous)) + ); // 12 + 9 = 21 + assert_eq!( + parse_military_timezone_with_offset("a5"), + Some((4, DayDelta::Same)) + ); // 23 + 5 = 28 % 24 = 4 + assert_eq!( + parse_military_timezone_with_offset("z"), + Some((0, DayDelta::Same)) + ); // UTC+0 -> 00:00 UTC + assert_eq!( + parse_military_timezone_with_offset("M9"), + Some((21, DayDelta::Previous)) + ); // Uppercase works // Invalid cases: 'j' reserved, empty, too long, starts with digit assert_eq!(parse_military_timezone_with_offset("j"), None); // Reserved for local time diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index 9a98b1b0309..b0613b14608 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -1132,6 +1132,46 @@ fn test_date_military_timezone_with_offset_variations() { } } +#[test] +fn test_date_military_timezone_with_offset_and_date() { + use chrono::{Duration, Utc}; + + let today = Utc::now().date_naive(); + + let test_cases = vec![ + ("m", -1), // M = UTC+12 + ("a", -1), // A = UTC+1 + ("n", 0), // N = UTC-1 + ("y", 0), // Y = UTC-12 + ("z", 0), // Z = UTC + // same day hour offsets + ("n2", 0), + // midnight crossings with hour offsets back to today + ("a1", 0), // exactly to midnight + ("a5", 0), // "overflow" midnight + ("m23", 0), + // midnight crossings with hour offsets to tomorrow + ("n23", 1), + ("y23", 1), + // midnight crossing to yesterday even with positive offset + ("m9", -1), // M = UTC+12 (-12 h + 9h is still `yesterday`) + ]; + + for (input, day_delta) in test_cases { + let expected_date = today.checked_add_signed(Duration::days(day_delta)).unwrap(); + + let expected = format!("{}\n", expected_date.format("%F")); + + new_ucmd!() + .env("TZ", "UTC") + .arg("-d") + .arg(input) + .arg("+%F") + .succeeds() + .stdout_is(expected); + } +} + // Locale-aware hour formatting tests #[test] #[cfg(unix)] From 9ca3afe57501113e2a95ad31dbaa2c021c08c9fb Mon Sep 17 00:00:00 2001 From: Max Ambaum Date: Fri, 2 Jan 2026 12:03:01 +0000 Subject: [PATCH 046/112] split: Added error when attempting to create file that already exists as directory (#9945) * split: Added error when attempting to create file that already exists as dir * split: Added integration test test_split::test_split_directory_already_exists * Fixed dependency error in windows.rs * Modified test to work on systems without /dev/zero * Attempt to fix windows error handling * Removed test for windows and made it more rigorous * Err made to look more like gnu * Updated test to reflect change in err message --- src/uu/split/locales/en-US.ftl | 1 + src/uu/split/src/platform/unix.rs | 11 +++++++---- src/uu/split/src/platform/windows.rs | 11 ++++++++--- tests/by-util/test_split.rs | 13 +++++++++++++ 4 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/uu/split/locales/en-US.ftl b/src/uu/split/locales/en-US.ftl index 4247eb5b9d9..629b8956d75 100644 --- a/src/uu/split/locales/en-US.ftl +++ b/src/uu/split/locales/en-US.ftl @@ -43,6 +43,7 @@ split-error-unable-to-reopen-file = unable to re-open { $file }; aborting split-error-file-descriptor-limit = at file descriptor limit, but no file descriptor left to close. Closed { $count } writers before. split-error-shell-process-returned = Shell process returned { $code } split-error-shell-process-terminated = Shell process terminated by signal +split-error-is-a-directory = { $dir }: Is a directory # Help messages for command-line options split-help-bytes = put SIZE bytes per output file diff --git a/src/uu/split/src/platform/unix.rs b/src/uu/split/src/platform/unix.rs index d1257954d3e..d530ee25966 100644 --- a/src/uu/split/src/platform/unix.rs +++ b/src/uu/split/src/platform/unix.rs @@ -4,8 +4,8 @@ // file that was distributed with this source code. use std::env; use std::ffi::OsStr; -use std::io::Write; use std::io::{BufWriter, Error, Result}; +use std::io::{ErrorKind, Write}; use std::path::Path; use std::process::{Child, Command, Stdio}; use uucore::error::USimpleError; @@ -139,10 +139,13 @@ pub fn instantiate_current_writer( .create(true) .truncate(true) .open(Path::new(&filename)) - .map_err(|_| { - Error::other( + .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), - ) + ), })? } else { // re-open file that we previously created to append to it diff --git a/src/uu/split/src/platform/windows.rs b/src/uu/split/src/platform/windows.rs index e443a9cfb3b..6693e4fe909 100644 --- a/src/uu/split/src/platform/windows.rs +++ b/src/uu/split/src/platform/windows.rs @@ -3,8 +3,8 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. use std::ffi::OsStr; -use std::io::Write; use std::io::{BufWriter, Error, Result}; +use std::io::{ErrorKind, Write}; use std::path::Path; use uucore::fs; use uucore::translate; @@ -25,8 +25,13 @@ pub fn instantiate_current_writer( .create(true) .truncate(true) .open(Path::new(&filename)) - .map_err(|_| { - Error::other(translate!("split-error-unable-to-open-file", "file" => 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)) + } })? } else { // re-open file that we previously created to append to it diff --git a/tests/by-util/test_split.rs b/tests/by-util/test_split.rs index f710e14425b..497559aca3e 100644 --- a/tests/by-util/test_split.rs +++ b/tests/by-util/test_split.rs @@ -2078,3 +2078,16 @@ fn test_split_non_utf8_additional_suffix() { "Expected at least one split file to be created" ); } + +#[test] +#[cfg(target_os = "linux")] // To re-enable on Windows once I work out what goes wrong with it. +fn test_split_directory_already_exists() { + let (at, mut ucmd) = at_and_ucmd!(); + + at.mkdir("xaa"); // For collision with. + at.touch("file"); + ucmd.args(&["file"]) + .fails_with_code(1) + .no_stdout() + .stderr_is("split: xaa: Is a directory\n"); +} From 0cb77a7541272c5b1a3bf8654db8f7d25137f8f3 Mon Sep 17 00:00:00 2001 From: "Tom D." Date: Sat, 27 Dec 2025 22:45:33 +0100 Subject: [PATCH 047/112] perf(tsort): avoid reading the whole input into memory and intern strings --- Cargo.lock | 12 ++ Cargo.toml | 3 +- src/uu/tsort/Cargo.toml | 5 +- src/uu/tsort/src/tsort.rs | 359 ++++++++++++++++++++++---------------- 4 files changed, 230 insertions(+), 149 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bf51e63c144..870a612840b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2711,6 +2711,16 @@ dependencies = [ "num-traits", ] +[[package]] +name = "string-interner" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23de088478b31c349c9ba67816fa55d9355232d63c3afea8bf513e31f0f1d2c0" +dependencies = [ + "hashbrown 0.15.4", + "serde", +] + [[package]] name = "strsim" version = "0.11.1" @@ -4043,6 +4053,8 @@ dependencies = [ "clap", "codspeed-divan-compat", "fluent", + "nix", + "string-interner", "tempfile", "thiserror 2.0.17", "uucore", diff --git a/Cargo.toml b/Cargo.toml index 80af38b85c6..6d34d5c1efe 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 serde bincode gethostid kqueue libselinux mangen memmap uuhelp startswith constness expl unnested logind cfgs +# spell-checker:ignore (libs) bigdecimal datetime serde bincode gethostid kqueue libselinux mangen memmap uuhelp startswith constness expl unnested logind cfgs interner [package] name = "coreutils" @@ -369,6 +369,7 @@ same-file = "1.0.6" self_cell = "1.0.4" # FIXME we use the exact version because the new 0.5.3 requires an MSRV of 1.88 selinux = "=0.5.2" +string-interner = "0.19.0" signal-hook = "0.4.1" tempfile = "3.15.0" terminal_size = "0.4.0" diff --git a/src/uu/tsort/Cargo.toml b/src/uu/tsort/Cargo.toml index 94b170223c1..72559199c8c 100644 --- a/src/uu/tsort/Cargo.toml +++ b/src/uu/tsort/Cargo.toml @@ -1,3 +1,4 @@ +#spell-checker:ignore (libs) interner [package] name = "uu_tsort" description = "tsort ~ (uutils) topologically sort input (partially ordered) pairs" @@ -19,9 +20,11 @@ path = "src/tsort.rs" [dependencies] clap = { workspace = true } +fluent = { workspace = true } +string-interner = { workspace = true } thiserror = { workspace = true } +nix = { workspace = true, features = ["fs"] } uucore = { workspace = true } -fluent = { workspace = true } [[bin]] name = "tsort" diff --git a/src/uu/tsort/src/tsort.rs b/src/uu/tsort/src/tsort.rs index 26c6f8ffc99..8eab8ab21c6 100644 --- a/src/uu/tsort/src/tsort.rs +++ b/src/uu/tsort/src/tsort.rs @@ -2,108 +2,95 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -//spell-checker:ignore TAOCP indegree +//spell-checker:ignore TAOCP indegree fadvise FADV +//spell-checker:ignore (libs) interner uclibc use clap::{Arg, ArgAction, Command}; use std::collections::hash_map::Entry; use std::collections::{HashMap, VecDeque}; use std::ffi::OsString; +use std::fs::File; +use std::io::{self, BufRead, BufReader}; use std::path::Path; +use string_interner::StringInterner; +use string_interner::backend::StringBackend; use thiserror::Error; use uucore::display::Quotable; use uucore::error::{UError, UResult, USimpleError}; -use uucore::{format_usage, show}; +use uucore::{format_usage, show, translate}; -use uucore::translate; +// short types for switching interning behavior on the fly. +type Sym = string_interner::symbol::SymbolU32; +type Interner = StringInterner>; mod options { pub const FILE: &str = "file"; } -#[derive(Debug, Error)] -enum TsortError { - /// The input file is actually a directory. - #[error("{input}: {message}", input = .0.maybe_quote(), message = translate!("tsort-error-is-dir"))] - IsDir(OsString), - - /// The number of tokens in the input data is odd. - /// - /// The list of edges must be even because each edge has two - /// components: a source node and a target node. - #[error("{input}: {message}", input = .0.maybe_quote(), message = translate!("tsort-error-odd"))] - NumTokensOdd(OsString), - - /// The graph contains a cycle. - #[error("{input}: {message}", input = .0.maybe_quote(), message = translate!("tsort-error-loop"))] - Loop(OsString), -} - -// Auxiliary struct, just for printing loop nodes via show! macro -#[derive(Debug, Error)] -#[error("{0}")] -struct LoopNode<'a>(&'a str); - -impl UError for TsortError {} -impl UError for LoopNode<'_> {} - #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?; - let mut inputs: Vec = matches + let mut inputs = matches .get_many::(options::FILE) - .map(|vals| vals.cloned().collect()) - .unwrap_or_default(); - - if inputs.is_empty() { - inputs.push(OsString::from("-")); - } - - if inputs.len() > 1 { - return Err(USimpleError::new( - 1, - translate!( - "tsort-error-extra-operand", - "operand" => inputs[1].quote(), - "util" => uucore::util_name() - ), - )); - } - - let input = inputs .into_iter() - .next() - .expect(translate!("tsort-error-at-least-one-input").as_str()); + .flatten(); + + let input = match (inputs.next(), inputs.next()) { + (None, _) => { + return Err(USimpleError::new( + 1, + translate!("tsort-error-at-least-one-input"), + )); + } + (Some(input), None) => input, + (Some(_), Some(extra)) => { + return Err(USimpleError::new( + 1, + translate!( + "tsort-error-extra-operand", + "operand" => extra.quote(), + "util" => uucore::util_name() + ), + )); + } + }; - let data = if input == "-" { - let stdin = std::io::stdin(); - std::io::read_to_string(stdin)? + // Create the directed graph from pairs of tokens in the input data. + let mut g = Graph::new(input.to_string_lossy().to_string()); + if input == "-" { + process_input(io::stdin().lock(), &mut g)?; } else { let path = Path::new(&input); if path.is_dir() { - return Err(TsortError::IsDir(input.clone()).into()); + return Err(TsortError::IsDir(input.to_string_lossy().to_string()).into()); } - std::fs::read_to_string(path)? - }; - // Create the directed graph from pairs of tokens in the input data. - let mut g = Graph::new(input.clone()); - // Input is considered to be in the format - // From1 To1 From2 To2 ... - // with tokens separated by whitespaces - let mut edge_tokens = data.split_whitespace(); - // Note: this is equivalent to iterating over edge_tokens.chunks(2) - // but chunks() exists only for slices and would require unnecessary Vec allocation. - // Itertools::chunks() is not used due to unnecessary overhead for internal RefCells - loop { - // Try take next pair of tokens - let Some(from) = edge_tokens.next() else { - // no more tokens -> end of input. Graph constructed - break; - }; - let Some(to) = edge_tokens.next() else { - return Err(TsortError::NumTokensOdd(input.clone()).into()); - }; - g.add_edge(from, to); + let file = File::open(path)?; + + // advise the OS we will access the data sequentially if available. + #[cfg(any( + target_os = "linux", + target_os = "android", + target_os = "fuchsia", + target_os = "wasi", + target_env = "uclibc", + target_os = "freebsd", + ))] + { + use nix::fcntl::{PosixFadviseAdvice, posix_fadvise}; + use std::os::unix::io::AsFd; + + posix_fadvise( + file.as_fd(), // file descriptor + 0, // start of the file + 0, // length 0 = all + PosixFadviseAdvice::POSIX_FADV_SEQUENTIAL, + ) + .ok(); + } + + let reader = BufReader::new(file); + process_input(reader, &mut g)?; } g.run_tsort(); @@ -117,6 +104,7 @@ pub fn uu_app() -> Command { .override_usage(format_usage(&translate!("tsort-usage"))) .about(translate!("tsort-about")) .infer_long_args(true) + // no-op flag, needed for POSIX compatibility. .arg( Arg::new("warn") .short('w') @@ -128,11 +116,69 @@ pub fn uu_app() -> Command { .hide(true) .value_parser(clap::value_parser!(OsString)) .value_hint(clap::ValueHint::FilePath) - .num_args(0..) + .default_value("-") + .num_args(1..) .action(ArgAction::Append), ) } +#[derive(Debug, Error)] +enum TsortError { + /// The input file is actually a directory. + #[error("{input}: {message}", input = .0.maybe_quote(), message = translate!("tsort-error-is-dir"))] + IsDir(String), + + /// The number of tokens in the input data is odd. + /// + /// The length of the list of edges must be even because each edge has two + /// components: a source node and a target node. + #[error("{input}: {message}", input = .0.maybe_quote(), message = translate!("tsort-error-odd"))] + NumTokensOdd(String), + + /// The graph contains a cycle. + #[error("{input}: {message}", input = .0, message = translate!("tsort-error-loop"))] + Loop(String), + + /// Wrapper for bubbling up IO errors + #[error("{0}")] + IO(#[from] std::io::Error), +} + +// Auxiliary struct, just for printing loop nodes via show! macro +#[derive(Debug, Error)] +#[error("{0}")] +struct LoopNode<'a>(&'a str); + +impl UError for TsortError {} +impl UError for LoopNode<'_> {} + +fn process_input(reader: R, graph: &mut Graph) -> Result<(), TsortError> { + let mut pending: Option = None; + + // Input is considered to be in the format + // From1 To1 From2 To2 ... + // with tokens separated by whitespaces + + for line in reader.lines() { + let line = line?; + for token in line.split_whitespace() { + // Intern the token and get a Sym + let token_sym = graph.interner.get_or_intern(token); + + if let Some(from) = pending.take() { + graph.add_edge(from, token_sym); + } else { + pending = Some(token_sym); + } + } + } + if pending.is_some() { + return Err(TsortError::NumTokensOdd(graph.name())); + } + + Ok(()) +} + /// Find the element `x` in `vec` and remove it, returning its index. fn remove(vec: &mut Vec, x: T) -> Option where @@ -143,40 +189,54 @@ where }) } -// We use String as a representation of node here -// but using integer may improve performance. +#[derive(Clone, Copy, PartialEq, Eq)] +enum VisitedState { + Opened, + Closed, +} + #[derive(Default)] -struct Node<'input> { - successor_names: Vec<&'input str>, +struct Node { + successor_tokens: Vec, predecessor_count: usize, } -impl<'input> Node<'input> { - fn add_successor(&mut self, successor_name: &'input str) { - self.successor_names.push(successor_name); +impl Node { + fn add_successor(&mut self, successor_name: Sym) { + self.successor_tokens.push(successor_name); } } -struct Graph<'input> { - name: OsString, - nodes: HashMap<&'input str, Node<'input>>, +struct Graph { + name_sym: Sym, + nodes: HashMap, + interner: Interner, } -#[derive(Clone, Copy, PartialEq, Eq)] -enum VisitedState { - Opened, - Closed, -} - -impl<'input> Graph<'input> { - fn new(name: OsString) -> Self { +impl Graph { + fn new(name: String) -> Self { + let mut interner = Interner::new(); + let name_sym = interner.get_or_intern(name); Self { - name, + name_sym, + interner, nodes: HashMap::default(), } } - fn add_edge(&mut self, from: &'input str, to: &'input str) { + fn name(&self) -> String { + //SAFETY: the name is interned during graph creation and stored as name_sym. + // gives much better performance on lookup. + unsafe { self.interner.resolve_unchecked(self.name_sym).to_owned() } + } + fn get_node_name(&self, node_sym: Sym) -> &str { + //SAFETY: the only way to get a Sym is by manipulating an interned string. + // gives much better performance on lookup. + + unsafe { self.interner.resolve_unchecked(node_sym) } + } + + fn add_edge(&mut self, from: Sym, to: Sym) { let from_node = self.nodes.entry(from).or_default(); if from != to { from_node.add_successor(to); @@ -185,71 +245,76 @@ impl<'input> Graph<'input> { } } - fn remove_edge(&mut self, u: &'input str, v: &'input str) { - remove(&mut self.nodes.get_mut(u).unwrap().successor_names, v); - self.nodes.get_mut(v).unwrap().predecessor_count -= 1; + fn remove_edge(&mut self, u: Sym, v: Sym) { + remove( + &mut self + .nodes + .get_mut(&u) + .expect("node is part of the graph") + .successor_tokens, + v, + ); + self.nodes + .get_mut(&v) + .expect("node is part of the graph") + .predecessor_count -= 1; } /// Implementation of algorithm T from TAOCP (Don. Knuth), vol. 1. fn run_tsort(&mut self) { - // First, we find nodes that have no prerequisites (independent nodes). - // If no such node exists, then there is a cycle. - let mut independent_nodes_queue: VecDeque<&'input str> = self + let mut independent_nodes_queue: VecDeque = self .nodes .iter() - .filter_map(|(&name, node)| { + .filter_map(|(&sym, node)| { if node.predecessor_count == 0 { - Some(name) + Some(sym) } else { None } }) .collect(); - // To make sure the resulting ordering is deterministic we - // need to order independent nodes. - // - // FIXME: this doesn't comply entirely with the GNU coreutils - // implementation. - independent_nodes_queue.make_contiguous().sort_unstable(); + // Sort by resolved string for deterministic output + independent_nodes_queue + .make_contiguous() + .sort_unstable_by(|a, b| self.get_node_name(*a).cmp(self.get_node_name(*b))); while !self.nodes.is_empty() { - // Get the next node (breaking any cycles necessary to do so). let v = self.find_next_node(&mut independent_nodes_queue); - println!("{v}"); - if let Some(node_to_process) = self.nodes.remove(v) { - for successor_name in node_to_process.successor_names.into_iter().rev() { - let successor_node = self.nodes.get_mut(successor_name).unwrap(); + println!("{}", self.get_node_name(v)); + if let Some(node_to_process) = self.nodes.remove(&v) { + for successor_name in node_to_process.successor_tokens.into_iter().rev() { + // we reverse to match GNU tsort order + let successor_node = self + .nodes + .get_mut(&successor_name) + .expect("node is part of the graph"); successor_node.predecessor_count -= 1; if successor_node.predecessor_count == 0 { - // If we find nodes without any other prerequisites, we add them to the queue. independent_nodes_queue.push_back(successor_name); } } } } } - - /// Get the in-degree of the node with the given name. - fn indegree(&self, name: &str) -> Option { - self.nodes.get(name).map(|data| data.predecessor_count) + pub fn indegree(&self, sym: Sym) -> Option { + self.nodes.get(&sym).map(|data| data.predecessor_count) } - // Pre-condition: self.nodes is non-empty. - fn find_next_node(&mut self, frontier: &mut VecDeque<&'input str>) -> &'input str { + fn find_next_node(&mut self, frontier: &mut VecDeque) -> Sym { // If there are no nodes of in-degree zero but there are still // un-visited nodes in the graph, then there must be a cycle. - // We need to find the cycle, display it, and then break the - // cycle. + // We need to find the cycle, display it on stderr, and break it to go on. // // A cycle is guaranteed to be of length at least two. We break // the cycle by deleting an arbitrary edge (the first). That is // not necessarily the optimal thing, but it should be enough to - // continue making progress in the graph traversal. + // continue making progress in the graph traversal, and matches GNU tsort behavior. // // It is possible that deleting the edge does not actually // result in the target node having in-degree zero, so we repeat // the process until such a node appears. + loop { match frontier.pop_front() { None => self.find_and_break_cycle(frontier), @@ -258,27 +323,28 @@ impl<'input> Graph<'input> { } } - fn find_and_break_cycle(&mut self, frontier: &mut VecDeque<&'input str>) { + fn find_and_break_cycle(&mut self, frontier: &mut VecDeque) { let cycle = self.detect_cycle(); - show!(TsortError::Loop(self.name.clone())); - for &node in &cycle { - show!(LoopNode(node)); + show!(TsortError::Loop(self.name())); + for &sym in &cycle { + show!(LoopNode(self.get_node_name(sym))); } let u = *cycle.last().expect("cycle must be non-empty"); let v = cycle[0]; self.remove_edge(u, v); - if self.indegree(v).unwrap() == 0 { + if self.indegree(v).expect("node is part of the graph") == 0 { frontier.push_back(v); } } - fn detect_cycle(&self) -> Vec<&'input str> { - let mut nodes: Vec<_> = self.nodes.keys().collect(); - nodes.sort_unstable(); + fn detect_cycle(&self) -> Vec { + // Sort by resolved string for deterministic output + let mut nodes: Vec<_> = self.nodes.keys().copied().collect(); + nodes.sort_unstable_by(|a, b| self.get_node_name(*a).cmp(self.get_node_name(*b))); let mut visited = HashMap::new(); let mut stack = Vec::with_capacity(self.nodes.len()); - for node in nodes { + for &node in &nodes { if self.dfs(node, &mut visited, &mut stack) { let (loop_entry, _) = stack.pop().expect("loop is not empty"); @@ -294,13 +360,15 @@ impl<'input> Graph<'input> { fn dfs<'a>( &'a self, - node: &'input str, - visited: &mut HashMap<&'input str, VisitedState>, - stack: &mut Vec<(&'input str, &'a [&'input str])>, + node: Sym, + visited: &mut HashMap, + stack: &mut Vec<(Sym, &'a [Sym])>, ) -> bool { stack.push(( node, - self.nodes.get(node).map_or(&[], |n| &n.successor_names), + self.nodes + .get(&node) + .map_or(&[], |n: &Node| &n.successor_tokens), )); let state = *visited.entry(node).or_insert(VisitedState::Opened); @@ -320,22 +388,19 @@ impl<'input> Graph<'input> { match visited.entry(next_node) { Entry::Vacant(v) => { - // It's a first time we enter this node + // first visit of the node v.insert(VisitedState::Opened); stack.push(( next_node, self.nodes - .get(next_node) - .map_or(&[], |n| &n.successor_names), + .get(&next_node) + .map_or(&[], |n| &n.successor_tokens), )); } Entry::Occupied(o) => { if *o.get() == VisitedState::Opened { - // we are entering the same opened node again -> loop found - // stack contains it - // - // But part of the stack may not be belonging to this loop - // push found node to the stack to be able to trace the beginning of the loop + // We have found a node that was already visited by another iteration => loop completed + // the stack may contain unrelated nodes. This allows narrowing the loop down. stack.push((next_node, &[])); return true; } From 4168f9a8f2f315ac2995c2dcdbc622a6891a1ab8 Mon Sep 17 00:00:00 2001 From: "Tom D." Date: Thu, 1 Jan 2026 22:34:51 +0100 Subject: [PATCH 048/112] perf(tsort): avoid redundant check on input --- src/uu/tsort/src/tsort.rs | 71 +++++++++++++++++++++++---------------- 1 file changed, 42 insertions(+), 29 deletions(-) diff --git a/src/uu/tsort/src/tsort.rs b/src/uu/tsort/src/tsort.rs index 8eab8ab21c6..50e8acb7596 100644 --- a/src/uu/tsort/src/tsort.rs +++ b/src/uu/tsort/src/tsort.rs @@ -10,7 +10,6 @@ use std::collections::{HashMap, VecDeque}; use std::ffi::OsString; use std::fs::File; use std::io::{self, BufRead, BufReader}; -use std::path::Path; use string_interner::StringInterner; use string_interner::backend::StringBackend; use thiserror::Error; @@ -54,41 +53,49 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { )); } }; - + let file: File; // Create the directed graph from pairs of tokens in the input data. let mut g = Graph::new(input.to_string_lossy().to_string()); if input == "-" { process_input(io::stdin().lock(), &mut g)?; } else { - let path = Path::new(&input); - if path.is_dir() { - return Err(TsortError::IsDir(input.to_string_lossy().to_string()).into()); - } + #[cfg(windows)] + { + use std::path::Path; + + let path = Path::new(input); + if path.is_dir() { + return Err(TsortError::IsDir(input.to_string_lossy().to_string()).into()); + } - let file = File::open(path)?; - - // advise the OS we will access the data sequentially if available. - #[cfg(any( - target_os = "linux", - target_os = "android", - target_os = "fuchsia", - target_os = "wasi", - target_env = "uclibc", - target_os = "freebsd", - ))] + file = File::open(path)?; + } + #[cfg(not(windows))] { - use nix::fcntl::{PosixFadviseAdvice, posix_fadvise}; - use std::os::unix::io::AsFd; - - posix_fadvise( - file.as_fd(), // file descriptor - 0, // start of the file - 0, // length 0 = all - PosixFadviseAdvice::POSIX_FADV_SEQUENTIAL, - ) - .ok(); + file = File::open(input)?; + + // advise the OS we will access the data sequentially if available. + #[cfg(any( + target_os = "linux", + target_os = "android", + target_os = "fuchsia", + target_os = "wasi", + target_env = "uclibc", + target_os = "freebsd", + ))] + { + use nix::fcntl::{PosixFadviseAdvice, posix_fadvise}; + use std::os::unix::io::AsFd; + + posix_fadvise( + file.as_fd(), // file descriptor + 0, // start of the file + 0, // length 0 = all + PosixFadviseAdvice::POSIX_FADV_SEQUENTIAL, + ) + .ok(); + } } - let reader = BufReader::new(file); process_input(reader, &mut g)?; } @@ -160,7 +167,13 @@ fn process_input(reader: R, graph: &mut Graph) -> Result<(), TsortEr // with tokens separated by whitespaces for line in reader.lines() { - let line = line?; + let line = line.map_err(|e| { + if e.kind() == io::ErrorKind::IsADirectory { + TsortError::IsDir(graph.name()) + } else { + e.into() + } + })?; for token in line.split_whitespace() { // Intern the token and get a Sym let token_sym = graph.interner.get_or_intern(token); From c74a83c15b2bddc1e9029443e4afd9937ab90496 Mon Sep 17 00:00:00 2001 From: "Tom D." Date: Fri, 2 Jan 2026 11:59:31 +0100 Subject: [PATCH 049/112] perf(tsort): switch to the Bucket interning Backend for better lookup performance --- src/uu/tsort/src/tsort.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/uu/tsort/src/tsort.rs b/src/uu/tsort/src/tsort.rs index 50e8acb7596..713c2f5c907 100644 --- a/src/uu/tsort/src/tsort.rs +++ b/src/uu/tsort/src/tsort.rs @@ -11,15 +11,15 @@ use std::ffi::OsString; use std::fs::File; use std::io::{self, BufRead, BufReader}; use string_interner::StringInterner; -use string_interner::backend::StringBackend; +use string_interner::backend::BucketBackend; use thiserror::Error; use uucore::display::Quotable; use uucore::error::{UError, UResult, USimpleError}; use uucore::{format_usage, show, translate}; // short types for switching interning behavior on the fly. -type Sym = string_interner::symbol::SymbolU32; -type Interner = StringInterner>; +type Sym = string_interner::symbol::SymbolUsize; +type Interner = StringInterner>; mod options { pub const FILE: &str = "file"; @@ -59,6 +59,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { if input == "-" { process_input(io::stdin().lock(), &mut g)?; } else { + // Windows reports a permission denied error when trying to read a directory. + // So we check manually beforehand. On other systems, we avoid this extra check for performance. #[cfg(windows)] { use std::path::Path; @@ -88,9 +90,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { use std::os::unix::io::AsFd; posix_fadvise( - file.as_fd(), // file descriptor - 0, // start of the file - 0, // length 0 = all + file.as_fd(), + 0, // offset 0 => from the start of the file + 0, // length 0 => for the whole file PosixFadviseAdvice::POSIX_FADV_SEQUENTIAL, ) .ok(); From c0f25231a81187cefa3449f3654e3427ad70d52d Mon Sep 17 00:00:00 2001 From: cerdelen <95369756+cerdelen@users.noreply.github.com> Date: Fri, 2 Jan 2026 16:38:14 +0100 Subject: [PATCH 050/112] rmdir: Remove all trailing slashes when checking for symlinks (#9983) * rmdir: Remove all trailing slashes when checking for symlinks rmdir: cargo fmt l * rmdir: Extract removal of trailing slashes to helper func * rmdir: Add regression test for removal of trailing slashes when checking for symlink * rmdir: add cfg flag to helper func which is only used on unix --- src/uu/rmdir/src/rmdir.rs | 14 ++++++++++++-- tests/by-util/test_rmdir.rs | 15 +++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/uu/rmdir/src/rmdir.rs b/src/uu/rmdir/src/rmdir.rs index 4f13afcbf83..e0c9f73bcec 100644 --- a/src/uu/rmdir/src/rmdir.rs +++ b/src/uu/rmdir/src/rmdir.rs @@ -66,10 +66,11 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { Ok(path.metadata()?.file_type().is_dir()) } - let bytes = path.as_os_str().as_bytes(); + let mut bytes = path.as_os_str().as_bytes(); if error.raw_os_error() == Some(libc::ENOTDIR) && bytes.ends_with(b"/") { // Strip the trailing slash or .symlink_metadata() will follow the symlink - let no_slash: &Path = OsStr::from_bytes(&bytes[..bytes.len() - 1]).as_ref(); + bytes = strip_trailing_slashes_from_path(bytes); + let no_slash: &Path = OsStr::from_bytes(bytes).as_ref(); if no_slash.is_symlink() && points_to_directory(no_slash).unwrap_or(true) { show_error!( "{}", @@ -119,6 +120,15 @@ fn remove_single(path: &Path, opts: Opts) -> Result<(), Error<'_>> { remove_dir(path).map_err(|error| Error { error, path }) } +#[cfg(unix)] +fn strip_trailing_slashes_from_path(path: &[u8]) -> &[u8] { + let mut end = path.len(); + while end > 0 && path[end - 1] == b'/' { + end -= 1; + } + &path[..end] +} + // POSIX: https://pubs.opengroup.org/onlinepubs/009696799/functions/rmdir.html #[cfg(not(windows))] const NOT_EMPTY_CODES: &[i32] = &[libc::ENOTEMPTY, libc::EEXIST]; diff --git a/tests/by-util/test_rmdir.rs b/tests/by-util/test_rmdir.rs index 0c52a22878e..669884488a0 100644 --- a/tests/by-util/test_rmdir.rs +++ b/tests/by-util/test_rmdir.rs @@ -243,3 +243,18 @@ fn test_rmdir_remove_symlink_dangling() { .fails() .stderr_is("rmdir: failed to remove 'dl/': Symbolic link not followed\n"); } + +#[cfg(any(target_os = "linux", target_os = "android"))] +#[test] +fn test_rmdir_remove_symlink_dir_with_trailing_slashes() { + // a symlink with trailing slashes should still be printing the 'Symbolic link not followed' + // message + let (at, mut ucmd) = at_and_ucmd!(); + + at.mkdir("dir"); + at.symlink_dir("dir", "dl"); + + ucmd.arg("dl////") + .fails() + .stderr_is("rmdir: failed to remove 'dl////': Symbolic link not followed\n"); +} From 5f0eb225b76cd8b07377e85fb595febe053f3f08 Mon Sep 17 00:00:00 2001 From: Rostyslav Toch Date: Fri, 2 Jan 2026 16:17:24 +0000 Subject: [PATCH 051/112] date: add benchmark (#9911) * date: add benchmark * date: register benchmark in github actions list --------- Co-authored-by: Sylvestre Ledru --- .github/workflows/benchmarks.yml | 1 + Cargo.lock | 2 + src/uu/date/Cargo.toml | 9 ++++ src/uu/date/benches/date_bench.rs | 76 +++++++++++++++++++++++++++++++ 4 files changed, 88 insertions(+) create mode 100644 src/uu/date/benches/date_bench.rs diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 76fe09b7a04..33588cd803f 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -45,6 +45,7 @@ jobs: - { package: uu_uniq } - { package: uu_wc } - { package: uu_factor } + - { package: uu_date } steps: - uses: actions/checkout@v6 with: diff --git a/Cargo.lock b/Cargo.lock index 870a612840b..e0c8bb88412 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3208,10 +3208,12 @@ name = "uu_date" version = "0.5.0" dependencies = [ "clap", + "codspeed-divan-compat", "fluent", "jiff", "nix", "parse_datetime", + "tempfile", "uucore", "windows-sys 0.61.2", ] diff --git a/src/uu/date/Cargo.toml b/src/uu/date/Cargo.toml index 431868b9175..9bff97696f0 100644 --- a/src/uu/date/Cargo.toml +++ b/src/uu/date/Cargo.toml @@ -41,3 +41,12 @@ windows-sys = { workspace = true, features = [ [[bin]] name = "date" path = "src/main.rs" + +[dev-dependencies] +divan = { workspace = true } +tempfile = { workspace = true } +uucore = { workspace = true, features = ["benchmark"] } + +[[bench]] +name = "date_bench" +harness = false diff --git a/src/uu/date/benches/date_bench.rs b/src/uu/date/benches/date_bench.rs new file mode 100644 index 00000000000..636f876c544 --- /dev/null +++ b/src/uu/date/benches/date_bench.rs @@ -0,0 +1,76 @@ +// 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 divan::{Bencher, black_box}; +use std::io::Write; +use tempfile::NamedTempFile; +use uu_date::uumain; +use uucore::benchmark::run_util_function; + +/// Helper to create a temporary file containing N lines of date strings. +fn setup_date_file(lines: usize, date_format: &str) -> NamedTempFile { + let mut file = NamedTempFile::new().unwrap(); + for _ in 0..lines { + writeln!(file, "{date_format}").unwrap(); + } + file +} + +/// Benchmarks processing a file containing simple ISO dates. +#[divan::bench(args = [100, 1_000, 10_000])] +fn file_iso_dates(bencher: Bencher, count: usize) { + let file = setup_date_file(count, "2023-05-10 12:00:00"); + let path = file.path().to_str().unwrap(); + + bencher.bench(|| { + black_box(run_util_function(uumain, &["-f", path])); + }); +} + +/// Benchmarks processing a file containing dates with Timezone abbreviations. +#[divan::bench(args = [100, 1_000, 10_000])] +fn file_tz_abbreviations(bencher: Bencher, count: usize) { + // "EST" triggers the abbreviation lookup and double-parsing logic + let file = setup_date_file(count, "2023-05-10 12:00:00 EST"); + let path = file.path().to_str().unwrap(); + + bencher.bench(|| { + black_box(run_util_function(uumain, &["-f", path])); + }); +} + +/// Benchmarks formatting speed using a custom output format. +#[divan::bench(args = [1_000])] +fn file_custom_format(bencher: Bencher, count: usize) { + let file = setup_date_file(count, "2023-05-10 12:00:00"); + let path = file.path().to_str().unwrap(); + + bencher.bench(|| { + black_box(run_util_function(uumain, &["-f", path, "+%A %d %B %Y"])); + }); +} + +/// Benchmarks the overhead of starting the utility for a single date (no file). +#[divan::bench] +fn single_date_now(bencher: Bencher) { + bencher.bench(|| { + black_box(run_util_function(uumain, &[])); + }); +} + +/// Benchmarks parsing a complex relative date string passed as an argument. +#[divan::bench] +fn complex_relative_date(bencher: Bencher) { + bencher.bench(|| { + black_box(run_util_function( + uumain, + &["--date=last friday 12:00 + 2 days"], + )); + }); +} + +fn main() { + divan::main(); +} From 25a20742b15d7330ea32f31c823acda025b3d52e Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Thu, 1 Jan 2026 15:34:41 +0100 Subject: [PATCH 052/112] GnuTests.yml: use minimal Rust profile in VM --- .github/workflows/GnuTests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/GnuTests.yml b/.github/workflows/GnuTests.yml index 0cac5365722..3a7bb002df6 100644 --- a/.github/workflows/GnuTests.yml +++ b/.github/workflows/GnuTests.yml @@ -246,7 +246,7 @@ jobs: run: | lima sudo dnf -y update lima sudo dnf -y install autoconf bison gperf gcc gdb jq libacl-devel libattr-devel libcap-devel libselinux-devel attr rustup clang-devel automake patch quilt - lima rustup-init -y --default-toolchain stable + lima rustup-init -y --profile=minimal --default-toolchain stable - name: Copy the sources to VM run: | rsync -a -e ssh . lima-default:~/work/ From a99f36f2cbbd544e3038784a5266e87ce05d8b57 Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Fri, 2 Jan 2026 17:06:10 +0100 Subject: [PATCH 053/112] benchmarks: use simulation mode instrumentation mode has been deprecated --- .github/workflows/benchmarks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 33588cd803f..e14de6c0201 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -73,7 +73,7 @@ jobs: env: CODSPEED_LOG: debug with: - mode: instrumentation + mode: simulation run: | echo "Running benchmarks for ${{ matrix.benchmark-target.package }}" cargo codspeed run -p ${{ matrix.benchmark-target.package }} > /dev/null From d2452d988d47b864266c38ee3049271edbd69df3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 3 Jan 2026 00:46:49 +0000 Subject: [PATCH 054/112] chore(deps): update rust crate clap to v4.5.54 --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e0c8bb88412..8934d048b0a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -345,18 +345,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.53" +version = "4.5.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" +checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.53" +version = "4.5.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" +checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" dependencies = [ "anstream", "anstyle", From 315f4836eba384cf02751621f54b04774f35a3e1 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 2 Jan 2026 18:57:28 +0100 Subject: [PATCH 055/112] date benchmark: keep only one value --- src/uu/date/benches/date_bench.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/uu/date/benches/date_bench.rs b/src/uu/date/benches/date_bench.rs index 636f876c544..1c1d05aaea2 100644 --- a/src/uu/date/benches/date_bench.rs +++ b/src/uu/date/benches/date_bench.rs @@ -19,8 +19,9 @@ fn setup_date_file(lines: usize, date_format: &str) -> NamedTempFile { } /// Benchmarks processing a file containing simple ISO dates. -#[divan::bench(args = [100, 1_000, 10_000])] -fn file_iso_dates(bencher: Bencher, count: usize) { +#[divan::bench] +fn file_iso_dates(bencher: Bencher) { + let count = 1_000; let file = setup_date_file(count, "2023-05-10 12:00:00"); let path = file.path().to_str().unwrap(); @@ -30,8 +31,9 @@ fn file_iso_dates(bencher: Bencher, count: usize) { } /// Benchmarks processing a file containing dates with Timezone abbreviations. -#[divan::bench(args = [100, 1_000, 10_000])] -fn file_tz_abbreviations(bencher: Bencher, count: usize) { +#[divan::bench] +fn file_tz_abbreviations(bencher: Bencher) { + let count = 1_000; // "EST" triggers the abbreviation lookup and double-parsing logic let file = setup_date_file(count, "2023-05-10 12:00:00 EST"); let path = file.path().to_str().unwrap(); @@ -42,8 +44,9 @@ fn file_tz_abbreviations(bencher: Bencher, count: usize) { } /// Benchmarks formatting speed using a custom output format. -#[divan::bench(args = [1_000])] -fn file_custom_format(bencher: Bencher, count: usize) { +#[divan::bench] +fn file_custom_format(bencher: Bencher) { + let count = 1_000; let file = setup_date_file(count, "2023-05-10 12:00:00"); let path = file.path().to_str().unwrap(); From 1a6600eed7c6fe9a491e611c9476b01f441f7392 Mon Sep 17 00:00:00 2001 From: oech3 <> Date: Sat, 3 Jan 2026 07:03:02 +0900 Subject: [PATCH 056/112] hashsum: Move --ckeck's deps to clap --- src/uu/hashsum/src/hashsum.rs | 33 +++++++++++++++------------------ tests/by-util/test_hashsum.rs | 6 +++--- util/build-gnu.sh | 2 ++ 3 files changed, 20 insertions(+), 21 deletions(-) diff --git a/src/uu/hashsum/src/hashsum.rs b/src/uu/hashsum/src/hashsum.rs index 19e8ad9db04..eea434d9477 100644 --- a/src/uu/hashsum/src/hashsum.rs +++ b/src/uu/hashsum/src/hashsum.rs @@ -157,19 +157,11 @@ pub fn uumain(mut args: impl uucore::Args) -> UResult<()> { }; let check = matches.get_flag("check"); - let check_flag = |flag| match (check, matches.get_flag(flag)) { - (_, false) => Ok(false), - (true, true) => Ok(true), - (false, true) => Err(ChecksumError::CheckOnlyFlag(flag.into())), - }; - - // Each of the following flags are only expected in --check mode. - // If we encounter them otherwise, end with an error. - let ignore_missing = check_flag("ignore-missing")?; - let warn = check_flag("warn")?; - let quiet = check_flag("quiet")?; - let strict = check_flag("strict")?; - let status = check_flag("status")?; + let ignore_missing = matches.get_flag("ignore-missing"); + let warn = matches.get_flag("warn"); + let quiet = matches.get_flag("quiet"); + let strict = matches.get_flag("strict"); + let status = matches.get_flag("status"); let files = matches.get_many::(options::FILE).map_or_else( // No files given, read from stdin. @@ -301,7 +293,8 @@ pub fn uu_app_common() -> Command { .long(options::QUIET) .help(translate!("hashsum-help-quiet")) .action(ArgAction::SetTrue) - .overrides_with_all([options::STATUS, options::WARN]), + .overrides_with_all([options::STATUS, options::WARN]) + .requires(options::CHECK), ) .arg( Arg::new(options::STATUS) @@ -309,19 +302,22 @@ pub fn uu_app_common() -> Command { .long("status") .help(translate!("hashsum-help-status")) .action(ArgAction::SetTrue) - .overrides_with_all([options::QUIET, options::WARN]), + .overrides_with_all([options::QUIET, options::WARN]) + .requires(options::CHECK), ) .arg( Arg::new(options::STRICT) .long("strict") .help(translate!("hashsum-help-strict")) - .action(ArgAction::SetTrue), + .action(ArgAction::SetTrue) + .requires(options::CHECK), ) .arg( Arg::new("ignore-missing") .long("ignore-missing") .help(translate!("hashsum-help-ignore-missing")) - .action(ArgAction::SetTrue), + .action(ArgAction::SetTrue) + .requires(options::CHECK), ) .arg( Arg::new(options::WARN) @@ -329,7 +325,8 @@ pub fn uu_app_common() -> Command { .long("warn") .help(translate!("hashsum-help-warn")) .action(ArgAction::SetTrue) - .overrides_with_all([options::QUIET, options::STATUS]), + .overrides_with_all([options::QUIET, options::STATUS]) + .requires(options::CHECK), ) .arg( Arg::new("zero") diff --git a/tests/by-util/test_hashsum.rs b/tests/by-util/test_hashsum.rs index 2f1719b0eca..891cb9d4dfd 100644 --- a/tests/by-util/test_hashsum.rs +++ b/tests/by-util/test_hashsum.rs @@ -268,7 +268,7 @@ fn test_check_md5_ignore_missing() { .arg("--ignore-missing") .arg(at.subdir.join("testf.sha1")) .fails() - .stderr_contains("the --ignore-missing option is meaningful only when verifying checksums"); + .stderr_contains("the following required arguments were not provided"); //clap generated error } #[test] @@ -1021,13 +1021,13 @@ fn test_check_quiet() { .arg("--quiet") .arg(at.subdir.join("in.md5")) .fails() - .stderr_contains("md5sum: the --quiet option is meaningful only when verifying checksums"); + .stderr_contains("the following required arguments were not provided"); //clap generated error scene .ccmd("md5sum") .arg("--strict") .arg(at.subdir.join("in.md5")) .fails() - .stderr_contains("md5sum: the --strict option is meaningful only when verifying checksums"); + .stderr_contains("the following required arguments were not provided"); //clap generated error } #[test] diff --git a/util/build-gnu.sh b/util/build-gnu.sh index 7e92396bfc3..2ae6b1e6186 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -322,6 +322,8 @@ test \$n_stat1 -ge \$n_stat2 \\' tests/ls/stat-free-color.sh # no need to replicate this output with hashsum "${SED}" -i -e "s|Try 'md5sum --help' for more information.\\\n||" tests/cksum/md5sum.pl +# clap changes the error message + "${SED}" -i '/check-ignore-missing-4/,/EXIT=> 1/ { /ERR=>/,/try_help/d }' tests/cksum/md5sum.pl # Our ls command always outputs ANSI color codes prepended with a zero. However, # in the case of GNU, it seems inconsistent. Nevertheless, it looks like it From 3e1cfd425cda61e03976831df2b209dd8a877bf0 Mon Sep 17 00:00:00 2001 From: CrazyRoka Date: Fri, 2 Jan 2026 21:49:33 +0000 Subject: [PATCH 057/112] date: avoid double parsing when resolving timezone abbreviations --- src/uu/date/src/date.rs | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/uu/date/src/date.rs b/src/uu/date/src/date.rs index d6623e5a654..389e269235a 100644 --- a/src/uu/date/src/date.rs +++ b/src/uu/date/src/date.rs @@ -671,9 +671,12 @@ fn tz_abbrev_to_iana(abbrev: &str) -> Option<&str> { cache.get(abbrev).map(|s| s.as_str()) } -/// Resolve timezone abbreviation in date string and replace with numeric offset. -/// Returns the modified string with offset, or original if no abbreviation found. -fn resolve_tz_abbreviation>(date_str: S) -> String { +/// Attempts to parse a date string that contains a timezone abbreviation (e.g. "EST"). +/// +/// If an abbreviation is found and the date is parsable, returns `Some(Zoned)`. +/// Returns `None` if no abbreviation is detected or if parsing fails, indicating +/// that standard parsing should be attempted. +fn try_parse_with_abbreviation>(date_str: S) -> Option { let s = date_str.as_ref(); // Look for timezone abbreviation at the end of the string @@ -697,11 +700,7 @@ fn resolve_tz_abbreviation>(date_str: S) -> String { let ts = parsed.timestamp(); // Get the offset for this specific timestamp in the target timezone - let zoned = ts.to_zoned(tz); - let offset_str = format!("{}", zoned.offset()); - - // Replace abbreviation with offset - return format!("{date_part} {offset_str}"); + return Some(ts.to_zoned(tz)); } } } @@ -709,7 +708,7 @@ fn resolve_tz_abbreviation>(date_str: S) -> String { } // No abbreviation found or couldn't resolve, return original - s.to_string() + None } /// Parse a `String` into a `DateTime`. @@ -724,10 +723,12 @@ fn resolve_tz_abbreviation>(date_str: S) -> String { fn parse_date + Clone>( s: S, ) -> Result { - // First, try to resolve any timezone abbreviations - let resolved = resolve_tz_abbreviation(s.as_ref()); + // First, try to parse any timezone abbreviations + if let Some(zoned) = try_parse_with_abbreviation(s.as_ref()) { + return Ok(zoned); + } - match parse_datetime::parse_datetime(&resolved) { + match parse_datetime::parse_datetime(s.as_ref()) { Ok(date) => { // Convert to system timezone for display // (parse_datetime 0.13 returns Zoned in the input's timezone) From b57fba172225847e3e625a32ef6452cbc971695c Mon Sep 17 00:00:00 2001 From: Christopher Dryden Date: Fri, 2 Jan 2026 20:20:01 +0000 Subject: [PATCH 058/112] pr: add -b flag for backwards compatibility --- src/uu/pr/src/pr.rs | 8 ++++++++ tests/by-util/test_pr.rs | 6 ++++++ 2 files changed, 14 insertions(+) diff --git a/src/uu/pr/src/pr.rs b/src/uu/pr/src/pr.rs index 19e9f2a0c89..fde2370480b 100644 --- a/src/uu/pr/src/pr.rs +++ b/src/uu/pr/src/pr.rs @@ -48,6 +48,7 @@ mod options { pub const COLUMN_WIDTH: &str = "width"; pub const PAGE_WIDTH: &str = "page-width"; pub const ACROSS: &str = "across"; + pub const COLUMN_DOWN: &str = "column-down"; pub const COLUMN: &str = "column"; pub const COLUMN_CHAR_SEPARATOR: &str = "separator"; pub const COLUMN_STRING_SEPARATOR: &str = "sep-string"; @@ -257,6 +258,13 @@ pub fn uu_app() -> Command { .help(translate!("pr-help-across")) .action(ArgAction::SetTrue), ) + .arg( + // -b is a no-op for backwards compatibility (column-down is now the default) + Arg::new(options::COLUMN_DOWN) + .short('b') + .hide(true) + .action(ArgAction::SetTrue), + ) .arg( Arg::new(options::COLUMN) .long(options::COLUMN) diff --git a/tests/by-util/test_pr.rs b/tests/by-util/test_pr.rs index 0bb161fb8a8..26f64e1dc5e 100644 --- a/tests/by-util/test_pr.rs +++ b/tests/by-util/test_pr.rs @@ -616,3 +616,9 @@ fn test_version() { fn test_pr_char_device_dev_null() { new_ucmd!().arg("/dev/null").succeeds(); } + +#[test] +fn test_b_flag_backwards_compat() { + // -b is a no-op for backwards compatibility (column-down is now the default) + new_ucmd!().args(&["-b", "-t"]).pipe_in("a\nb\n").succeeds(); +} From 309c7416714d5bdeccfb53616db7a89bee2e74a2 Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Fri, 2 Jan 2026 15:07:13 +0100 Subject: [PATCH 059/112] uucore/uptime: remove unreachable code on Windows --- src/uucore/src/lib/features/uptime.rs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/uucore/src/lib/features/uptime.rs b/src/uucore/src/lib/features/uptime.rs index cc4d976ae9f..9dbf878d7e8 100644 --- a/src/uucore/src/lib/features/uptime.rs +++ b/src/uucore/src/lib/features/uptime.rs @@ -338,13 +338,8 @@ pub fn get_nusers() -> usize { continue; } - let username = if !buffer.is_null() { - let cstr = std::ffi::CStr::from_ptr(buffer as *const i8); - cstr.to_string_lossy().to_string() - } else { - String::new() - }; - if !username.is_empty() { + let cstr = std::ffi::CStr::from_ptr(buffer.cast()); + if !cstr.is_empty() { num_user += 1; } From 0723187f8e1ea6bb5c53bbe9c425e6a054d44d1a Mon Sep 17 00:00:00 2001 From: cerdelen <95369756+cerdelen@users.noreply.github.com> Date: Sat, 3 Jan 2026 11:40:51 +0100 Subject: [PATCH 060/112] Merge pull request #9990 from cerdelen/chmod_recursive_hyper_nested_dirs Chmod recursive hyper nested dirs --- src/uu/chmod/src/chmod.rs | 12 +++++++++--- tests/by-util/test_chmod.rs | 10 ++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/uu/chmod/src/chmod.rs b/src/uu/chmod/src/chmod.rs index b7e0f3fd965..b77de93f2b7 100644 --- a/src/uu/chmod/src/chmod.rs +++ b/src/uu/chmod/src/chmod.rs @@ -432,14 +432,20 @@ impl Chmoder { // If the path is a directory (or we should follow symlinks), recurse into it if (!file_path.is_symlink() || should_follow_symlink) && file_path.is_dir() { + // We buffer all paths in this dir to not keep to be able to close the fd so not + // too many fd's are open during the recursion + let mut paths_in_this_dir = Vec::new(); + for dir_entry in file_path.read_dir()? { - let path = match dir_entry { - Ok(entry) => entry.path(), + match dir_entry { + Ok(entry) => paths_in_this_dir.push(entry.path()), Err(err) => { r = r.and(Err(err.into())); continue; } - }; + } + } + for path in paths_in_this_dir { if path.is_symlink() { r = self.handle_symlink_during_recursion(&path).and(r); } else { diff --git a/tests/by-util/test_chmod.rs b/tests/by-util/test_chmod.rs index 5e340732832..6d242020ce3 100644 --- a/tests/by-util/test_chmod.rs +++ b/tests/by-util/test_chmod.rs @@ -407,6 +407,16 @@ fn test_chmod_recursive_correct_exit_code() { .stderr_is(err_msg); } +#[test] +fn test_chmod_hyper_recursive_directory_tree_does_not_fail() { + let (at, mut ucmd) = at_and_ucmd!(); + let mkdir = "a/".repeat(400); + + at.mkdir_all(&mkdir); + + ucmd.arg("-R").arg("777").arg("a").succeeds(); +} + #[test] #[allow(clippy::unreadable_literal)] fn test_chmod_recursive() { From 9f79c1950defb9c39f4c5037fe81dc39b7266712 Mon Sep 17 00:00:00 2001 From: Ramon <55579979+van-sprundel@users.noreply.github.com> Date: Sat, 3 Jan 2026 11:47:06 +0100 Subject: [PATCH 061/112] df: add binfmt_misc to is_dummy_filesystem (#9975) * df: add binfmt_misc to is_dummy_filesystem * df: add spell-checker ignore for binfmt * uucore: rmaix flag --- src/uucore/src/lib/features/fsext.rs | 14 +++++++-- tests/by-util/test_df.rs | 47 +++++++++++++++++++++++++++- 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/src/uucore/src/lib/features/fsext.rs b/src/uucore/src/lib/features/fsext.rs index ce734ff2d32..f2ae59a76f3 100644 --- a/src/uucore/src/lib/features/fsext.rs +++ b/src/uucore/src/lib/features/fsext.rs @@ -380,7 +380,7 @@ impl From for MountInfo { } } -#[cfg(all(unix, not(any(target_os = "aix", target_os = "redox"))))] +#[cfg(all(unix, not(target_os = "redox")))] fn is_dummy_filesystem(fs_type: &str, mount_option: &str) -> bool { // spell-checker:disable match fs_type { @@ -392,7 +392,9 @@ fn is_dummy_filesystem(fs_type: &str, mount_option: &str) -> bool { // for NetBSD 3.0 | "kernfs" // for Irix 6.5 - | "ignore" => true, + | "ignore" + // Binary format support pseudo-filesystem + | "binfmt_misc" => true, _ => fs_type == "none" && !mount_option.contains(MOUNT_OPT_BIND) } @@ -1220,4 +1222,12 @@ mod tests { crate::os_str_from_bytes(b"/mnt/some- -dir-\xf3").unwrap() ); } + + #[test] + #[cfg(all(unix, not(target_os = "redox")))] + // spell-checker:ignore (word) binfmt + fn test_binfmt_misc_is_dummy() { + use super::is_dummy_filesystem; + assert!(is_dummy_filesystem("binfmt_misc", "")); + } } diff --git a/tests/by-util/test_df.rs b/tests/by-util/test_df.rs index 8b305ce4273..4754acbfe41 100644 --- a/tests/by-util/test_df.rs +++ b/tests/by-util/test_df.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 udev pcent iuse itotal iused ipcent +// spell-checker:ignore udev pcent iuse itotal iused ipcent binfmt #![allow( clippy::similar_names, clippy::cast_possible_truncation, @@ -1046,3 +1046,48 @@ fn test_nonexistent_file() { .stderr_is("df: does-not-exist: No such file or directory\n") .stdout_is("File\n.\n"); } + +#[test] +#[cfg(target_os = "linux")] +fn test_df_all_shows_binfmt_misc() { + // Check if binfmt_misc is mounted + let is_mounted = std::fs::read_to_string("/proc/self/mountinfo") + .map(|content| content.lines().any(|line| line.contains("binfmt_misc"))) + .unwrap_or(false); + + if is_mounted { + let output = new_ucmd!() + .args(&["--all", "--output=fstype,target"]) + .succeeds() + .stdout_str_lossy(); + + assert!( + output.contains("binfmt_misc"), + "Expected binfmt_misc filesystem to appear in df --all output when it's mounted" + ); + } + // If binfmt_misc is not mounted, skip the test silently +} + +#[test] +#[cfg(target_os = "linux")] +fn test_df_hides_binfmt_misc_by_default() { + // Check if binfmt_misc is mounted + let is_mounted = std::fs::read_to_string("/proc/self/mountinfo") + .map(|content| content.lines().any(|line| line.contains("binfmt_misc"))) + .unwrap_or(false); + + if is_mounted { + let output = new_ucmd!() + .args(&["--output=fstype,target"]) + .succeeds() + .stdout_str_lossy(); + + // binfmt_misc should NOT appear in the output without --all + assert!( + !output.contains("binfmt_misc"), + "Expected binfmt_misc filesystem to be hidden in df output without --all" + ); + } + // If binfmt_misc is not mounted, skip the test silently +} From a5ee4087b67275f4d7ac2d3661f4bf3ba1460def Mon Sep 17 00:00:00 2001 From: WaterWhisperer Date: Sat, 3 Jan 2026 20:22:18 +0800 Subject: [PATCH 062/112] join: add benchmarks (#10005) --- .github/workflows/benchmarks.yml | 1 + Cargo.lock | 2 + src/uu/join/Cargo.toml | 9 +++ src/uu/join/benches/join_bench.rs | 115 ++++++++++++++++++++++++++++++ 4 files changed, 127 insertions(+) create mode 100644 src/uu/join/benches/join_bench.rs diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index e14de6c0201..9f53a016773 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -31,6 +31,7 @@ jobs: - { package: uu_du } - { package: uu_expand } - { package: uu_fold } + - { package: uu_join } - { package: uu_ls } - { package: uu_mv } - { package: uu_nl } diff --git a/Cargo.lock b/Cargo.lock index 8934d048b0a..47ecd75af57 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3462,8 +3462,10 @@ name = "uu_join" version = "0.5.0" dependencies = [ "clap", + "codspeed-divan-compat", "fluent", "memchr", + "tempfile", "thiserror 2.0.17", "uucore", ] diff --git a/src/uu/join/Cargo.toml b/src/uu/join/Cargo.toml index cc93d5e18b1..401cb3bb57c 100644 --- a/src/uu/join/Cargo.toml +++ b/src/uu/join/Cargo.toml @@ -27,3 +27,12 @@ fluent = { workspace = true } [[bin]] name = "join" path = "src/main.rs" + +[dev-dependencies] +divan = { workspace = true } +tempfile = { workspace = true } +uucore = { workspace = true, features = ["benchmark"] } + +[[bench]] +name = "join_bench" +harness = false diff --git a/src/uu/join/benches/join_bench.rs b/src/uu/join/benches/join_bench.rs new file mode 100644 index 00000000000..efa316fd290 --- /dev/null +++ b/src/uu/join/benches/join_bench.rs @@ -0,0 +1,115 @@ +// 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 divan::{Bencher, black_box}; +use std::{fs::File, io::Write}; +use tempfile::TempDir; +use uu_join::uumain; +use uucore::benchmark::run_util_function; + +/// Create two sorted files with matching keys for join benchmarking +fn create_join_files(temp_dir: &TempDir, num_lines: usize) -> (String, String) { + let file1_path = temp_dir.path().join("file1.txt"); + let file2_path = temp_dir.path().join("file2.txt"); + + let mut file1 = File::create(&file1_path).unwrap(); + let mut file2 = File::create(&file2_path).unwrap(); + + for i in 0..num_lines { + writeln!(file1, "{i:08} field1_{i} field2_{i}").unwrap(); + writeln!(file2, "{i:08} data1_{i} data2_{i}").unwrap(); + } + + ( + file1_path.to_str().unwrap().to_string(), + file2_path.to_str().unwrap().to_string(), + ) +} + +/// Create two files with partial overlap for join benchmarking +fn create_partial_overlap_files( + temp_dir: &TempDir, + num_lines: usize, + overlap_ratio: f64, +) -> (String, String) { + let file1_path = temp_dir.path().join("file1.txt"); + let file2_path = temp_dir.path().join("file2.txt"); + + let mut file1 = File::create(&file1_path).unwrap(); + let mut file2 = File::create(&file2_path).unwrap(); + + let overlap_count = (num_lines as f64 * overlap_ratio) as usize; + + // File 1: keys 0 to num_lines-1 + for i in 0..num_lines { + writeln!(file1, "{i:08} f1_data_{i}").unwrap(); + } + + // File 2: keys (num_lines - overlap_count) to (2*num_lines - overlap_count - 1) + let start = num_lines - overlap_count; + for i in 0..num_lines { + writeln!(file2, "{:08} f2_data_{}", start + i, i).unwrap(); + } + + ( + file1_path.to_str().unwrap().to_string(), + file2_path.to_str().unwrap().to_string(), + ) +} + +/// Benchmark basic join with fully matching keys +#[divan::bench] +fn join_full_match(bencher: Bencher) { + let num_lines = 10000; + let temp_dir = TempDir::new().unwrap(); + let (file1, file2) = create_join_files(&temp_dir, num_lines); + + bencher.bench(|| { + black_box(run_util_function(uumain, &[&file1, &file2])); + }); +} + +/// Benchmark join with partial overlap (50%) +#[divan::bench] +fn join_partial_overlap(bencher: Bencher) { + let num_lines = 10000; + let temp_dir = TempDir::new().unwrap(); + let (file1, file2) = create_partial_overlap_files(&temp_dir, num_lines, 0.5); + + bencher.bench(|| { + black_box(run_util_function(uumain, &[&file1, &file2])); + }); +} + +/// Benchmark join with custom field separator +#[divan::bench] +fn join_custom_separator(bencher: Bencher) { + let num_lines = 10000; + let temp_dir = TempDir::new().unwrap(); + let file1_path = temp_dir.path().join("file1.txt"); + let file2_path = temp_dir.path().join("file2.txt"); + + let mut file1 = File::create(&file1_path).unwrap(); + let mut file2 = File::create(&file2_path).unwrap(); + + for i in 0..num_lines { + writeln!(file1, "{i:08}\tfield1_{i}\tfield2_{i}").unwrap(); + writeln!(file2, "{i:08}\tdata1_{i}\tdata2_{i}").unwrap(); + } + + let file1_str = file1_path.to_str().unwrap(); + let file2_str = file2_path.to_str().unwrap(); + + bencher.bench(|| { + black_box(run_util_function( + uumain, + &["-t", "\t", file1_str, file2_str], + )); + }); +} + +fn main() { + divan::main(); +} From 60ddff8fc4d57b7e0e48c838a792f07c37e88f77 Mon Sep 17 00:00:00 2001 From: oech3 <> Date: Sat, 3 Jan 2026 06:31:37 +0900 Subject: [PATCH 063/112] cksum: Move --ckeck's deps by clap --- src/uu/cksum/src/cksum.rs | 33 +++++++++++++++------------------ 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/src/uu/cksum/src/cksum.rs b/src/uu/cksum/src/cksum.rs index 30eabcaac56..bb3e3251114 100644 --- a/src/uu/cksum/src/cksum.rs +++ b/src/uu/cksum/src/cksum.rs @@ -140,19 +140,11 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let check = matches.get_flag(options::CHECK); - let check_flag = |flag| match (check, matches.get_flag(flag)) { - (_, false) => Ok(false), - (true, true) => Ok(true), - (false, true) => Err(ChecksumError::CheckOnlyFlag(flag.into())), - }; - - // Each of the following flags are only expected in --check mode. - // If we encounter them otherwise, end with an error. - let ignore_missing = check_flag(options::IGNORE_MISSING)?; - let warn = check_flag(options::WARN)?; - let quiet = check_flag(options::QUIET)?; - let strict = check_flag(options::STRICT)?; - let status = check_flag(options::STATUS)?; + let ignore_missing = matches.get_flag(options::IGNORE_MISSING); + let warn = matches.get_flag(options::WARN); + let quiet = matches.get_flag(options::QUIET); + let strict = matches.get_flag(options::STRICT); + let status = matches.get_flag(options::STATUS); let algo_cli = matches .get_one::(options::ALGORITHM) @@ -284,7 +276,8 @@ pub fn uu_app() -> Command { Arg::new(options::STRICT) .long(options::STRICT) .help(translate!("cksum-help-strict")) - .action(ArgAction::SetTrue), + .action(ArgAction::SetTrue) + .requires(options::CHECK), ) .arg( Arg::new(options::CHECK) @@ -324,27 +317,31 @@ pub fn uu_app() -> Command { .long("warn") .help(translate!("cksum-help-warn")) .action(ArgAction::SetTrue) - .overrides_with_all([options::STATUS, options::QUIET]), + .overrides_with_all([options::STATUS, options::QUIET]) + .requires(options::CHECK), ) .arg( Arg::new(options::STATUS) .long("status") .help(translate!("cksum-help-status")) .action(ArgAction::SetTrue) - .overrides_with_all([options::WARN, options::QUIET]), + .overrides_with_all([options::WARN, options::QUIET]) + .requires(options::CHECK), ) .arg( Arg::new(options::QUIET) .long(options::QUIET) .help(translate!("cksum-help-quiet")) .action(ArgAction::SetTrue) - .overrides_with_all([options::WARN, options::STATUS]), + .overrides_with_all([options::WARN, options::STATUS]) + .requires(options::CHECK), ) .arg( Arg::new(options::IGNORE_MISSING) .long(options::IGNORE_MISSING) .help(translate!("cksum-help-ignore-missing")) - .action(ArgAction::SetTrue), + .action(ArgAction::SetTrue) + .requires(options::CHECK), ) .arg( Arg::new(options::ZERO) From d3ea22f4692939cb84cbe4420839ca041180bfca Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Sat, 3 Jan 2026 21:23:14 +0900 Subject: [PATCH 064/112] Merge pull request #9999 from oech3/cksum-text-clap-untagged cksum: Move handle_tag_text_binary_flags to clap --- src/uu/cksum/src/cksum.rs | 46 ++++++------------------------------- tests/by-util/test_cksum.rs | 2 +- 2 files changed, 8 insertions(+), 40 deletions(-) diff --git a/src/uu/cksum/src/cksum.rs b/src/uu/cksum/src/cksum.rs index bb3e3251114..3d814ae6f24 100644 --- a/src/uu/cksum/src/cksum.rs +++ b/src/uu/cksum/src/cksum.rs @@ -74,42 +74,6 @@ mod options { /// Returns a pair of boolean. The first one indicates if we should use tagged /// output format, the second one indicates if we should use the binary flag in /// the untagged case. -fn handle_tag_text_binary_flags>( - args: impl Iterator, -) -> UResult<(bool, bool)> { - let mut tag = true; - let mut binary = false; - let mut text = false; - - // --binary, --tag and --untagged are tight together: none of them - // conflicts with each other but --tag will reset "binary" and "text" and - // set "tag". - - for arg in args { - let arg = arg.as_ref(); - if arg == "-b" || arg == "--binary" { - text = false; - binary = true; - } else if arg == "--text" { - text = true; - binary = false; - } else if arg == "--tag" { - tag = true; - binary = false; - text = false; - } else if arg == "--untagged" { - tag = false; - } - } - - // Specifying --text without ever mentioning --untagged fails. - if text && tag { - return Err(ChecksumError::TextWithoutUntagged.into()); - } - - Ok((tag, binary)) -} - /// Sanitize the `--length` argument depending on `--algorithm` and `--length`. fn maybe_sanitize_length( algo_cli: Option, @@ -200,7 +164,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // Set the default algorithm to CRC when not '--check'ing. let algo_kind = algo_cli.unwrap_or(AlgoKind::Crc); - let (tag, binary) = handle_tag_text_binary_flags(std::env::args_os())?; + let tag = matches.get_flag(options::TAG) || !matches.get_flag(options::UNTAGGED); + let binary = matches.get_flag(options::BINARY); let algo = SizedAlgoKind::from_unsized(algo_kind, length)?; let line_ending = LineEnding::from_zero_flag(matches.get_flag(options::ZERO)); @@ -257,7 +222,9 @@ pub fn uu_app() -> Command { .long(options::TAG) .help(translate!("cksum-help-tag")) .action(ArgAction::SetTrue) - .overrides_with(options::UNTAGGED), + .overrides_with(options::UNTAGGED) + .overrides_with(options::BINARY) + .overrides_with(options::TEXT), ) .arg( Arg::new(options::LENGTH) @@ -301,7 +268,8 @@ pub fn uu_app() -> Command { .short('t') .hide(true) .overrides_with(options::BINARY) - .action(ArgAction::SetTrue), + .action(ArgAction::SetTrue) + .requires(options::UNTAGGED), ) .arg( Arg::new(options::BINARY) diff --git a/tests/by-util/test_cksum.rs b/tests/by-util/test_cksum.rs index d4685d6198f..d1abe3409ba 100644 --- a/tests/by-util/test_cksum.rs +++ b/tests/by-util/test_cksum.rs @@ -1066,7 +1066,7 @@ mod output_format { .args(&["-a", "md5"]) .arg(at.subdir.join("f")) .fails_with_code(1) - .stderr_contains("--text mode is only supported with --untagged"); + .stderr_contains("the following required arguments were not provided"); //clap does not change the meaning } #[test] From 64ffd977bf346c660c57346568f3417b3ba59941 Mon Sep 17 00:00:00 2001 From: WaterWhisperer Date: Sat, 3 Jan 2026 21:45:11 +0800 Subject: [PATCH 065/112] join: add benchmark with the French locale (#10025) --- src/uu/join/benches/join_bench.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/uu/join/benches/join_bench.rs b/src/uu/join/benches/join_bench.rs index efa316fd290..798f4344fb0 100644 --- a/src/uu/join/benches/join_bench.rs +++ b/src/uu/join/benches/join_bench.rs @@ -110,6 +110,22 @@ fn join_custom_separator(bencher: Bencher) { }); } +/// Benchmark join with French locale (fr_FR.UTF-8) +#[divan::bench] +fn join_french_locale(bencher: Bencher) { + let num_lines = 10000; + let temp_dir = TempDir::new().unwrap(); + let (file1, file2) = create_join_files(&temp_dir, num_lines); + + bencher + .with_inputs(|| unsafe { + std::env::set_var("LC_ALL", "fr_FR.UTF-8"); + }) + .bench_values(|_| { + black_box(run_util_function(uumain, &[&file1, &file2])); + }); +} + fn main() { divan::main(); } From 4479afe5a057ee567f96d7973d79f86de42aa92d Mon Sep 17 00:00:00 2001 From: ffgan Date: Sun, 4 Jan 2026 00:57:57 +0800 Subject: [PATCH 066/112] CI: Default build artifact for riscv64+musl on CICD.yml (#10029) Co-authored by: nijincheng@iscas.ac.cn; Signed-off-by: ffgan --- .cargo/config.toml | 2 ++ .github/workflows/CICD.yml | 9 ++++++++- Cross.toml | 3 +++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index 364776950c7..803f6249978 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -6,6 +6,8 @@ linker = "x86_64-unknown-redox-gcc" [target.aarch64-unknown-linux-gnu] linker = "aarch64-linux-gnu-gcc" +[target.riscv64gc-unknown-linux-musl] +rustflags = ["-C", "target-feature=+crt-static"] [env] # See feat_external_libstdbuf in src/uu/stdbuf/Cargo.toml diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index e6a7fd4509e..67169f984ed 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -309,7 +309,7 @@ jobs: ./target/release-fast/true # Check that the progs have prefix test -f /tmp/usr/local/bin/uu-tty - test -f /tmp/usr/local/libexec/uu-coreutils/libstdbuf.* + test -f /tmp/usr/local/libexec/uu-coreutils/libstdbuf.* # Check that the manpage is not present ! test -f /tmp/usr/local/share/man/man1/uu-whoami.1 # Check that the completion is not present @@ -576,6 +576,7 @@ jobs: - { os: ubuntu-latest , target: arm-unknown-linux-gnueabihf , features: feat_os_unix_gnueabihf , use-cross: use-cross , skip-tests: true } - { os: ubuntu-24.04-arm , target: aarch64-unknown-linux-gnu , features: feat_os_unix_gnueabihf } - { os: ubuntu-latest , target: aarch64-unknown-linux-musl , features: feat_os_unix_musl , use-cross: use-cross , skip-tests: true } + - { os: ubuntu-latest , target: riscv64gc-unknown-linux-musl , features: feat_os_unix_musl , use-cross: use-cross , skip-tests: true } # - { os: ubuntu-latest , target: x86_64-unknown-linux-gnu , features: feat_selinux , use-cross: use-cross } - { os: ubuntu-latest , target: i686-unknown-linux-gnu , features: "feat_os_unix,test_risky_names", use-cross: use-cross } - { os: ubuntu-latest , target: i686-unknown-linux-musl , features: feat_os_unix_musl , use-cross: use-cross } @@ -636,6 +637,7 @@ jobs: unset TARGET_ARCH case '${{ matrix.job.target }}' in aarch64-*) TARGET_ARCH=arm64 ;; + riscv64gc-*) TARGET_ARCH=riscv64 ;; arm-*-*hf) TARGET_ARCH=armhf ;; i686-*) TARGET_ARCH=i686 ;; x86_64-*) TARGET_ARCH=x86_64 ;; @@ -700,6 +702,7 @@ jobs: STRIP="strip" case ${{ matrix.job.target }} in aarch64-*-linux-*) STRIP="aarch64-linux-gnu-strip" ;; + riscv64gc-*-linux-*) STRIP="riscv64-linux-gnu-strip" ;; arm-*-linux-gnueabihf) STRIP="arm-linux-gnueabihf-strip" ;; *-pc-windows-msvc) STRIP="" ;; esac; @@ -726,6 +729,10 @@ jobs: sudo apt-get -y update sudo apt-get -y install gcc-aarch64-linux-gnu ;; + riscv64gc-unknown-linux-*) + sudo apt-get -y update + sudo apt-get -y install gcc-riscv64-linux-gnu + ;; *-redox*) sudo apt-get -y update sudo apt-get -y install fuse3 libfuse-dev diff --git a/Cross.toml b/Cross.toml index 52f5bad21dd..90d824e61aa 100644 --- a/Cross.toml +++ b/Cross.toml @@ -5,3 +5,6 @@ pre-build = [ ] [build.env] passthrough = ["CI", "RUST_BACKTRACE", "CARGO_TERM_COLOR"] + +[target.riscv64gc-unknown-linux-musl] +image = "ghcr.io/cross-rs/riscv64gc-unknown-linux-musl:main" From a446cb27b53eba5caf490bf17fe0287711689b46 Mon Sep 17 00:00:00 2001 From: Haowei Hsu Date: Sun, 4 Jan 2026 00:58:45 +0800 Subject: [PATCH 067/112] style(uudoc): update header formatting for options and examples (#10004) Adjust the formatting of the options and examples sections in the uudoc output to use Markdown headers: - Change `

Options

` to `## Options` - Change `Examples` to `## Examples` - Add regression test to prevent reverting to HTML headers --- src/bin/uudoc.rs | 6 ++++-- tests/uudoc/mod.rs | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/bin/uudoc.rs b/src/bin/uudoc.rs index 5a713e040fa..392375f9edb 100644 --- a/src/bin/uudoc.rs +++ b/src/bin/uudoc.rs @@ -465,7 +465,9 @@ impl MDWriter<'_, '_> { /// # Errors /// Returns an error if the writer fails. fn options(&mut self) -> io::Result<()> { - writeln!(self.w, "

Options

")?; + writeln!(self.w)?; + writeln!(self.w, "## Options")?; + writeln!(self.w)?; write!(self.w, "
")?; for arg in self.command.get_arguments() { write!(self.w, "
")?; @@ -576,7 +578,7 @@ fn format_examples(content: String, output_markdown: bool) -> ResultOptions"), + "Generated markdown should not contain '

Options

' (use markdown format instead)" + ); + + // Also verify Examples if it exists + if content.contains("## Examples") { + assert!( + content.contains("## Examples"), + "Generated markdown should contain '## Examples' header in markdown format" + ); + } + } +} From d3f384dc3fc32d89743c6c0fba214092a18c655e Mon Sep 17 00:00:00 2001 From: Rostyslav Toch Date: Sat, 3 Jan 2026 17:00:04 +0000 Subject: [PATCH 068/112] perf(date): wrap stdout in BufWriter to improve batch processing (#9994) --- src/uu/date/locales/en-US.ftl | 1 + src/uu/date/locales/fr-FR.ftl | 1 + src/uu/date/src/date.rs | 19 +++++++++++++------ 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/uu/date/locales/en-US.ftl b/src/uu/date/locales/en-US.ftl index 80f82649da2..782275fec6e 100644 --- a/src/uu/date/locales/en-US.ftl +++ b/src/uu/date/locales/en-US.ftl @@ -105,3 +105,4 @@ date-error-setting-date-not-supported-macos = setting the date is not supported date-error-setting-date-not-supported-redox = setting the date is not supported by Redox date-error-cannot-set-date = cannot set date date-error-extra-operand = extra operand '{$operand}' +date-error-write = write error: {$error} diff --git a/src/uu/date/locales/fr-FR.ftl b/src/uu/date/locales/fr-FR.ftl index 1967c958a2b..15321c1fcdc 100644 --- a/src/uu/date/locales/fr-FR.ftl +++ b/src/uu/date/locales/fr-FR.ftl @@ -100,3 +100,4 @@ date-error-setting-date-not-supported-macos = la définition de la date n'est pa date-error-setting-date-not-supported-redox = la définition de la date n'est pas prise en charge par Redox date-error-cannot-set-date = impossible de définir la date date-error-extra-operand = opérande supplémentaire '{$operand}' +date-error-write = erreur d'écriture: {$error} diff --git a/src/uu/date/src/date.rs b/src/uu/date/src/date.rs index 389e269235a..5baa75432b6 100644 --- a/src/uu/date/src/date.rs +++ b/src/uu/date/src/date.rs @@ -13,7 +13,7 @@ use jiff::tz::{TimeZone, TimeZoneDatabase}; use jiff::{Timestamp, Zoned}; use std::collections::HashMap; use std::fs::File; -use std::io::{BufRead, BufReader}; +use std::io::{BufRead, BufReader, BufWriter, Write}; use std::path::PathBuf; use std::sync::OnceLock; use uucore::display::Quotable; @@ -428,24 +428,31 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { }; let format_string = make_format_string(&settings); + let mut stdout = BufWriter::new(std::io::stdout().lock()); // Format all the dates for date in dates { match date { // TODO: Switch to lenient formatting. Ok(date) => match strtime::format(format_string, &date) { - Ok(s) => println!("{s}"), + Ok(s) => writeln!(stdout, "{s}").map_err(|e| { + USimpleError::new(1, translate!("date-error-write", "error" => e)) + })?, Err(e) => { + let _ = stdout.flush(); return Err(USimpleError::new( 1, translate!("date-error-invalid-format", "format" => format_string, "error" => e), )); } }, - Err((input, _err)) => show!(USimpleError::new( - 1, - translate!("date-error-invalid-date", "date" => input) - )), + Err((input, _err)) => { + let _ = stdout.flush(); + show!(USimpleError::new( + 1, + translate!("date-error-invalid-date", "date" => input) + )); + } } } From 12f1675027e1be5e429e49ae66492e913c86e1c6 Mon Sep 17 00:00:00 2001 From: "Guillem L. Jara" <4lon3ly0@tutanota.com> Date: Wed, 29 Oct 2025 14:46:00 +0100 Subject: [PATCH 069/112] fix(stat): constrain mount point fetching Fixes issue #9072, where some AppArmor profiles designed for GNU coreutils would break under uutils because we would fetch this info unnecessarily. --- src/uu/stat/src/stat.rs | 49 +++++++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/src/uu/stat/src/stat.rs b/src/uu/stat/src/stat.rs index 327e89a6888..8a304485e69 100644 --- a/src/uu/stat/src/stat.rs +++ b/src/uu/stat/src/stat.rs @@ -908,6 +908,28 @@ impl Stater { Ok(tokens) } + fn populate_mount_list() -> UResult> { + let mut mount_list = read_fs_list() + .map_err(|e| { + USimpleError::new( + e.code(), + StatError::CannotReadFilesystem { + error: e.to_string(), + } + .to_string(), + ) + })? + .iter() + .map(|mi| mi.mount_dir.clone()) + .collect::>(); + + // Reverse sort. The longer comes first. + mount_list.sort(); + mount_list.reverse(); + + Ok(mount_list) + } + fn new(matches: &ArgMatches) -> UResult { let files: Vec = matches .get_many::(options::FILES) @@ -938,27 +960,16 @@ impl Stater { let default_dev_tokens = Self::generate_tokens(&Self::default_format(show_fs, terse, true), use_printf)?; - let mount_list = if show_fs { - // mount points aren't displayed when showing filesystem information + // mount points aren't displayed when showing filesystem information, or + // whenever the format string does not request the mount point. + let mount_list = if show_fs + || !default_tokens + .iter() + .any(|tok| matches!(tok, Token::Directive { format: 'm', .. })) + { None } else { - let mut mount_list = read_fs_list() - .map_err(|e| { - USimpleError::new( - e.code(), - StatError::CannotReadFilesystem { - error: e.to_string(), - } - .to_string(), - ) - })? - .iter() - .map(|mi| mi.mount_dir.clone()) - .collect::>(); - // Reverse sort. The longer comes first. - mount_list.sort(); - mount_list.reverse(); - Some(mount_list) + Some(Self::populate_mount_list()?) }; Ok(Self { From 589b1f2af4bcc393213aa4c41cb4938f83a7f486 Mon Sep 17 00:00:00 2001 From: "Guillem L. Jara" <4lon3ly0@tutanota.com> Date: Wed, 29 Oct 2025 15:11:35 +0100 Subject: [PATCH 070/112] fix(stat): fix default opts and several format errors Fixes issue #9071, where the default options for the `Device` field where incorrect, as well as several format flags that were not working as intended: %R, %Hr, %Lr, %Hd, %Ld, %t. --- src/uu/stat/src/stat.rs | 38 ++++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/src/uu/stat/src/stat.rs b/src/uu/stat/src/stat.rs index 8a304485e69..b8ffb61b970 100644 --- a/src/uu/stat/src/stat.rs +++ b/src/uu/stat/src/stat.rs @@ -70,6 +70,8 @@ struct Flags { space: bool, sign: bool, group: bool, + major: bool, + minor: bool, } /// checks if the string is within the specified bound, @@ -299,6 +301,16 @@ fn group_num(s: &str) -> Cow<'_, str> { res.into() } +/// Keeps major part of an integer +fn major(n: u64) -> u64 { + (n >> 8) & 0xFF +} + +// Keeps minor part of an integer +fn minor(n: u64) -> u64 { + n & 0xFF +} + struct Stater { follow: bool, show_fs: bool, @@ -794,13 +806,14 @@ impl Stater { if let Some(&next_char) = chars.get(*i + 1) { if (chars[*i] == 'H' || chars[*i] == 'L') && (next_char == 'd' || next_char == 'r') { - let specifier = format!("{}{next_char}", chars[*i]); + flag.major = chars[*i] == 'H'; + flag.minor = chars[*i] == 'L'; *i += 1; return Ok(Token::Directive { flag, width, precision, - format: specifier.chars().next().unwrap(), + format: next_char, }); } } @@ -1063,6 +1076,8 @@ impl Stater { } } // device number in decimal + 'd' if flag.major => OutputType::Unsigned(major(meta.dev())), + 'd' if flag.minor => OutputType::Unsigned(minor(meta.dev())), 'd' => OutputType::Unsigned(meta.dev()), // device number in hex 'D' => OutputType::UnsignedHex(meta.dev()), @@ -1101,10 +1116,10 @@ impl Stater { 's' => OutputType::Integer(meta.len() as i64), // major device type in hex, for character/block device special // files - 't' => OutputType::UnsignedHex(meta.rdev() >> 8), + 't' => OutputType::UnsignedHex(major(meta.rdev())), // minor device type in hex, for character/block device special // files - 'T' => OutputType::UnsignedHex(meta.rdev() & 0xff), + 'T' => OutputType::UnsignedHex(minor(meta.rdev())), // user ID of owner 'u' => OutputType::Unsigned(meta.uid() as u64), // user name of owner @@ -1147,15 +1162,10 @@ impl Stater { .map_or((0, 0), system_time_to_sec); OutputType::Float(sec as f64 + nsec as f64 / 1_000_000_000.0) } - 'R' => { - let major = meta.rdev() >> 8; - let minor = meta.rdev() & 0xff; - OutputType::Str(format!("{major},{minor}")) - } + 'R' => OutputType::UnsignedHex(meta.rdev()), + 'r' if flag.major => OutputType::Unsigned(major(meta.rdev())), + 'r' if flag.minor => OutputType::Unsigned(minor(meta.rdev())), 'r' => OutputType::Unsigned(meta.rdev()), - 'H' => OutputType::Unsigned(meta.rdev() >> 8), // Major in decimal - 'L' => OutputType::Unsigned(meta.rdev() & 0xff), // Minor in decimal - _ => OutputType::Unknown, }; print_it(&output, flag, width, precision); @@ -1280,7 +1290,7 @@ impl Stater { } else { let device_line = if show_dev_type { format!( - "{}: %Dh/%dd\t{}: %-10i {}: %-5h {} {}: %t,%T\n", + "{}: %Hd,%Ld\t{}: %-10i {}: %-5h {} {}: %t,%T\n", translate!("stat-word-device"), translate!("stat-word-inode"), translate!("stat-word-links"), @@ -1289,7 +1299,7 @@ impl Stater { ) } else { format!( - "{}: %Dh/%dd\t{}: %-10i {}: %h\n", + "{}: %Hd,%Ld\t{}: %-10i {}: %h\n", translate!("stat-word-device"), translate!("stat-word-inode"), translate!("stat-word-links") From b2ebf436d79d7958db2c9afc34a853e3690c6f89 Mon Sep 17 00:00:00 2001 From: "Guillem L. Jara" <4lon3ly0@tutanota.com> Date: Wed, 29 Oct 2025 15:36:47 +0100 Subject: [PATCH 071/112] fix(stat): fix % escaping Previosuly, stat would ignore the next char after escaping it; e.g., "%%m" would become "%" instead of literally "%m". --- src/uu/stat/src/stat.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/uu/stat/src/stat.rs b/src/uu/stat/src/stat.rs index b8ffb61b970..aa670ff097c 100644 --- a/src/uu/stat/src/stat.rs +++ b/src/uu/stat/src/stat.rs @@ -751,7 +751,6 @@ impl Stater { return Ok(Token::Char('%')); } if chars[*i] == '%' { - *i += 1; return Ok(Token::Char('%')); } From 22b5c1fc440479ad3651db7432cfc0372f4ed1ef Mon Sep 17 00:00:00 2001 From: "Guillem L. Jara" <4lon3ly0@tutanota.com> Date: Wed, 29 Oct 2025 19:55:32 +0100 Subject: [PATCH 072/112] chore(stat): Add test case for percent escaping --- tests/by-util/test_stat.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/by-util/test_stat.rs b/tests/by-util/test_stat.rs index 0aad7361bb8..eafa3ce4943 100644 --- a/tests/by-util/test_stat.rs +++ b/tests/by-util/test_stat.rs @@ -567,3 +567,14 @@ fn test_mount_point_combined_with_other_specifiers() { "Should print mount point, file name, and size" ); } + +#[cfg(unix)] +#[test] +fn test_percent_escaping() { + let ts = TestScenario::new(util_name!()); + let result = ts + .ucmd() + .args(&["--printf", "%%%m%%m%m%%%", "/bin/sh"]) + .succeeds(); + assert_eq!(result.stdout_str(), "%/%m/%%"); +} From 308d33ae6dd7c35e6012ba65cb5693511c50dd36 Mon Sep 17 00:00:00 2001 From: "Guillem L. Jara" <4lon3ly0@tutanota.com> Date: Wed, 31 Dec 2025 09:28:11 +0100 Subject: [PATCH 073/112] fix(stat, mknod): replace custom (flawed in stat) logic with libc's. Now uucore::fs reexports libc's major(), minor() and makedev() directives. --- .../cspell.dictionaries/jargon.wordlist.txt | 1 + src/uu/mknod/Cargo.toml | 2 +- src/uu/mknod/src/mknod.rs | 9 ++----- src/uu/stat/src/stat.rs | 24 ++++++------------- src/uucore/src/lib/features/fs.rs | 20 ++++++++++++++++ 5 files changed, 31 insertions(+), 25 deletions(-) diff --git a/.vscode/cspell.dictionaries/jargon.wordlist.txt b/.vscode/cspell.dictionaries/jargon.wordlist.txt index 9fa0b625aac..a1bda0e7690 100644 --- a/.vscode/cspell.dictionaries/jargon.wordlist.txt +++ b/.vscode/cspell.dictionaries/jargon.wordlist.txt @@ -86,6 +86,7 @@ listxattr llistxattr lossily lstat +makedev mebi mebibytes mergeable diff --git a/src/uu/mknod/Cargo.toml b/src/uu/mknod/Cargo.toml index 50e7e2fce3c..32b983134e3 100644 --- a/src/uu/mknod/Cargo.toml +++ b/src/uu/mknod/Cargo.toml @@ -21,7 +21,7 @@ path = "src/mknod.rs" [dependencies] clap = { workspace = true } libc = { workspace = true } -uucore = { workspace = true, features = ["mode"] } +uucore = { workspace = true, features = ["mode", "fs"] } fluent = { workspace = true } [features] diff --git a/src/uu/mknod/src/mknod.rs b/src/uu/mknod/src/mknod.rs index cc22aee5f5c..8a4cf82d01e 100644 --- a/src/uu/mknod/src/mknod.rs +++ b/src/uu/mknod/src/mknod.rs @@ -13,6 +13,7 @@ use std::ffi::CString; use uucore::display::Quotable; use uucore::error::{UResult, USimpleError, UUsageError, set_exit_code}; use uucore::format_usage; +use uucore::fs::makedev; use uucore::translate; const MODE_RW_UGO: mode_t = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH; @@ -26,12 +27,6 @@ mod options { pub const CONTEXT: &str = "context"; } -#[inline(always)] -fn makedev(maj: u64, min: u64) -> dev_t { - // pick up from - ((min & 0xff) | ((maj & 0xfff) << 8) | ((min & !0xff) << 12) | ((maj & !0xfff) << 32)) as dev_t -} - #[derive(Clone, PartialEq)] enum FileType { Block, @@ -145,7 +140,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { translate!("mknod-error-fifo-no-major-minor"), )); } - (_, Some(&major), Some(&minor)) => makedev(major, minor), + (_, Some(&major), Some(&minor)) => makedev(major as _, minor as _), _ => { return Err(UUsageError::new( 1, diff --git a/src/uu/stat/src/stat.rs b/src/uu/stat/src/stat.rs index aa670ff097c..bae89461cb8 100644 --- a/src/uu/stat/src/stat.rs +++ b/src/uu/stat/src/stat.rs @@ -9,7 +9,7 @@ use uucore::translate; use clap::builder::ValueParser; use uucore::display::Quotable; -use uucore::fs::display_permissions; +use uucore::fs::{display_permissions, major, minor}; use uucore::fsext::{ FsMeta, MetadataTimeField, StatFs, metadata_get_time, pretty_filetype, pretty_fstype, read_fs_list, statfs, @@ -301,16 +301,6 @@ fn group_num(s: &str) -> Cow<'_, str> { res.into() } -/// Keeps major part of an integer -fn major(n: u64) -> u64 { - (n >> 8) & 0xFF -} - -// Keeps minor part of an integer -fn minor(n: u64) -> u64 { - n & 0xFF -} - struct Stater { follow: bool, show_fs: bool, @@ -1075,8 +1065,8 @@ impl Stater { } } // device number in decimal - 'd' if flag.major => OutputType::Unsigned(major(meta.dev())), - 'd' if flag.minor => OutputType::Unsigned(minor(meta.dev())), + 'd' if flag.major => OutputType::Unsigned(major(meta.dev() as _) as u64), + 'd' if flag.minor => OutputType::Unsigned(minor(meta.dev() as _) as u64), 'd' => OutputType::Unsigned(meta.dev()), // device number in hex 'D' => OutputType::UnsignedHex(meta.dev()), @@ -1115,10 +1105,10 @@ impl Stater { 's' => OutputType::Integer(meta.len() as i64), // major device type in hex, for character/block device special // files - 't' => OutputType::UnsignedHex(major(meta.rdev())), + 't' => OutputType::UnsignedHex(major(meta.rdev() as _) as u64), // minor device type in hex, for character/block device special // files - 'T' => OutputType::UnsignedHex(minor(meta.rdev())), + 'T' => OutputType::UnsignedHex(minor(meta.rdev() as _) as u64), // user ID of owner 'u' => OutputType::Unsigned(meta.uid() as u64), // user name of owner @@ -1162,8 +1152,8 @@ impl Stater { OutputType::Float(sec as f64 + nsec as f64 / 1_000_000_000.0) } 'R' => OutputType::UnsignedHex(meta.rdev()), - 'r' if flag.major => OutputType::Unsigned(major(meta.rdev())), - 'r' if flag.minor => OutputType::Unsigned(minor(meta.rdev())), + 'r' if flag.major => OutputType::Unsigned(major(meta.rdev() as _) as u64), + 'r' if flag.minor => OutputType::Unsigned(minor(meta.rdev() as _) as u64), 'r' => OutputType::Unsigned(meta.rdev()), _ => OutputType::Unknown, }; diff --git a/src/uucore/src/lib/features/fs.rs b/src/uucore/src/lib/features/fs.rs index 16de054a3c2..9c710858082 100644 --- a/src/uucore/src/lib/features/fs.rs +++ b/src/uucore/src/lib/features/fs.rs @@ -13,6 +13,8 @@ use libc::{ S_IRUSR, S_ISGID, S_ISUID, S_ISVTX, S_IWGRP, S_IWOTH, S_IWUSR, S_IXGRP, S_IXOTH, S_IXUSR, mkfifo, mode_t, }; +#[cfg(all(unix, not(target_os = "redox")))] +pub use libc::{major, makedev, minor}; use std::collections::HashSet; use std::collections::VecDeque; use std::env; @@ -839,6 +841,24 @@ pub fn make_fifo(path: &Path) -> std::io::Result<()> { } } +// Redox's libc appears not to include the following utilities + +#[cfg(target_os = "redox")] +pub fn major(dev: libc::dev_t) -> libc::c_uint { + (((dev >> 8) & 0xFFF) | ((dev >> 32) & 0xFFFFF000)) as _ +} + +#[cfg(target_os = "redox")] +pub fn minor(dev: libc::dev_t) -> libc::c_uint { + ((dev & 0xFF) | ((dev >> 12) & 0xFFFFF00)) as _ +} + +#[cfg(target_os = "redox")] +pub fn makedev(maj: libc::c_uint, min: libc::c_uint) -> libc::dev_t { + let [maj, min] = [maj as libc::dev_t, min as libc::dev_t]; + (min & 0xff) | ((maj & 0xfff) << 8) | ((min & !0xff) << 12) | ((maj & !0xfff) << 32) +} + #[cfg(test)] mod tests { // Note this useful idiom: importing names from outer (for mod tests) scope. From 22f7231c11ad868adf435fabcd7c6e6c93341cc5 Mon Sep 17 00:00:00 2001 From: "Guillem L. Jara" <4lon3ly0@tutanota.com> Date: Wed, 31 Dec 2025 20:29:57 +0100 Subject: [PATCH 074/112] chore(stat): add test case for file metadata --- tests/by-util/test_stat.rs | 54 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/tests/by-util/test_stat.rs b/tests/by-util/test_stat.rs index eafa3ce4943..8347d49c795 100644 --- a/tests/by-util/test_stat.rs +++ b/tests/by-util/test_stat.rs @@ -9,6 +9,9 @@ use uutests::unwrap_or_return; use uutests::util::{TestScenario, expected_result}; use uutests::util_name; +use std::fs::metadata; +use std::os::unix::fs::MetadataExt; + #[test] fn test_invalid_arg() { new_ucmd!().arg("--definitely-invalid").fails_with_code(1); @@ -578,3 +581,54 @@ fn test_percent_escaping() { .succeeds(); assert_eq!(result.stdout_str(), "%/%m/%%"); } + +#[cfg(unix)] +#[test] +fn test_correct_metadata() { + use uucore::fs::{major, minor}; + + let ts = TestScenario::new(util_name!()); + let parse = |(i, str): (usize, &str)| { + // Some outputs (%[fDRtT]) are in hex; they're redundant, but we might + // as well also test case conversion. + let radix = if matches!(i, 2 | 10 | 14..) { 16 } else { 10 }; + i128::from_str_radix(str, radix) + }; + for device in ["/", "/dev/null"] { + let metadata = metadata(device).unwrap(); + // We avoid time vals because of fs race conditions, especially with + // access time and status time (this previously killed an otherwise + // perfect 11-hour-long CI run...). The large number of as-casts is + // due to inconsistencies on some platforms (read: BSDs), and we use + // i128 as a lowest-common denominator. + let test_str = "%u %g %f %b %s %h %i %d %Hd %Ld %D %r %Hr %Lr %R %t %T"; + let expected = [ + metadata.uid() as _, + metadata.gid() as _, + metadata.mode() as _, + metadata.blocks() as _, + metadata.size() as _, + metadata.nlink() as _, + metadata.ino() as _, + metadata.dev() as _, + major(metadata.dev() as _) as _, + minor(metadata.dev() as _) as _, + metadata.dev() as _, + metadata.rdev() as _, + major(metadata.rdev() as _) as _, + minor(metadata.rdev() as _) as _, + metadata.rdev() as _, + major(metadata.rdev() as _) as _, + minor(metadata.rdev() as _) as _, + ]; + let result = ts.ucmd().args(&["--printf", test_str, device]).succeeds(); + let output = result + .stdout_str() + .split(' ') + .enumerate() + .map(parse) + .collect::, _>>() + .unwrap(); + assert_eq!(output, &expected); + } +} From 3cf9410150e76e63f405fa75c934c85e97705a17 Mon Sep 17 00:00:00 2001 From: Ruiyang Wang Date: Sat, 3 Jan 2026 13:43:29 -0800 Subject: [PATCH 075/112] cat: fix write error handling to propagate errors instead of panicking The write helper functions (write_to_end, write_tab_to_end, write_nonprint_to_end) were using .unwrap() on write operations, which would cause a panic if writing failed. This changes them to return io::Result and use ? to properly propagate errors. Changes: - write_to_end: returns io::Result, uses ? instead of .unwrap() - write_tab_to_end: returns io::Result, uses ? instead of .unwrap() - write_nonprint_to_end: returns io::Result, uses ? instead of .unwrap() - write_end: returns io::Result to propagate errors from helpers - Updated call site in write_lines to handle the Result with ? - Updated unit tests to call .unwrap() on the Result Fixes #10016 --- src/uu/cat/src/cat.rs | 49 +++++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/src/uu/cat/src/cat.rs b/src/uu/cat/src/cat.rs index 26b28d916b3..3497429d2de 100644 --- a/src/uu/cat/src/cat.rs +++ b/src/uu/cat/src/cat.rs @@ -565,7 +565,7 @@ fn write_lines( } // print to end of line or end of buffer - let offset = write_end(&mut writer, &in_buf[pos..], options); + let offset = write_end(&mut writer, &in_buf[pos..], options)?; // end of buffer? if offset + pos == in_buf.len() { @@ -628,7 +628,11 @@ fn write_new_line( Ok(()) } -fn write_end(writer: &mut W, in_buf: &[u8], options: &OutputOptions) -> usize { +fn write_end( + writer: &mut W, + in_buf: &[u8], + options: &OutputOptions, +) -> io::Result { if options.show_nonprint { write_nonprint_to_end(in_buf, writer, options.tab().as_bytes()) } else if options.show_tabs { @@ -644,21 +648,21 @@ fn write_end(writer: &mut W, in_buf: &[u8], options: &OutputOptions) - // however, write_nonprint_to_end doesn't need to stop at \r because it will always write \r as ^M. // Return the number of written symbols -fn write_to_end(in_buf: &[u8], writer: &mut W) -> usize { +fn write_to_end(in_buf: &[u8], writer: &mut W) -> io::Result { // using memchr2 significantly improves performances match memchr2(b'\n', b'\r', in_buf) { Some(p) => { - writer.write_all(&in_buf[..p]).unwrap(); - p + writer.write_all(&in_buf[..p])?; + Ok(p) } None => { - writer.write_all(in_buf).unwrap(); - in_buf.len() + writer.write_all(in_buf)?; + Ok(in_buf.len()) } } } -fn write_tab_to_end(mut in_buf: &[u8], writer: &mut W) -> usize { +fn write_tab_to_end(mut in_buf: &[u8], writer: &mut W) -> io::Result { let mut count = 0; loop { match in_buf @@ -666,25 +670,25 @@ fn write_tab_to_end(mut in_buf: &[u8], writer: &mut W) -> usize { .position(|c| *c == b'\n' || *c == b'\t' || *c == b'\r') { Some(p) => { - writer.write_all(&in_buf[..p]).unwrap(); + writer.write_all(&in_buf[..p])?; if in_buf[p] == b'\t' { - writer.write_all(b"^I").unwrap(); + writer.write_all(b"^I")?; in_buf = &in_buf[p + 1..]; count += p + 1; } else { // b'\n' or b'\r' - return count + p; + return Ok(count + p); } } None => { - writer.write_all(in_buf).unwrap(); - return in_buf.len() + count; + writer.write_all(in_buf)?; + return Ok(in_buf.len() + count); } } } } -fn write_nonprint_to_end(in_buf: &[u8], writer: &mut W, tab: &[u8]) -> usize { +fn write_nonprint_to_end(in_buf: &[u8], writer: &mut W, tab: &[u8]) -> io::Result { let mut count = 0; for byte in in_buf.iter().copied() { @@ -699,11 +703,10 @@ fn write_nonprint_to_end(in_buf: &[u8], writer: &mut W, tab: &[u8]) -> 128..=159 => writer.write_all(&[b'M', b'-', b'^', byte - 64]), 160..=254 => writer.write_all(&[b'M', b'-', byte - 128]), _ => writer.write_all(b"M-^?"), - } - .unwrap(); + }?; count += 1; } - count + Ok(count) } fn write_end_of_line( @@ -733,14 +736,14 @@ mod tests { fn test_write_tab_to_end_with_newline() { let mut writer = BufWriter::with_capacity(1024 * 64, stdout()); let in_buf = b"a\tb\tc\n"; - assert_eq!(super::write_tab_to_end(in_buf, &mut writer), 5); + assert_eq!(super::write_tab_to_end(in_buf, &mut writer).unwrap(), 5); } #[test] fn test_write_tab_to_end_no_newline() { let mut writer = BufWriter::with_capacity(1024 * 64, stdout()); let in_buf = b"a\tb\tc"; - assert_eq!(super::write_tab_to_end(in_buf, &mut writer), 5); + assert_eq!(super::write_tab_to_end(in_buf, &mut writer).unwrap(), 5); } #[test] @@ -748,7 +751,7 @@ mod tests { let mut writer = BufWriter::with_capacity(1024 * 64, stdout()); let in_buf = b"\n"; let tab = b""; - super::write_nonprint_to_end(in_buf, &mut writer, tab); + super::write_nonprint_to_end(in_buf, &mut writer, tab).unwrap(); assert_eq!(writer.buffer().len(), 0); } @@ -757,7 +760,7 @@ mod tests { let mut writer = BufWriter::with_capacity(1024 * 64, stdout()); let in_buf = &[9u8]; let tab = b"tab"; - super::write_nonprint_to_end(in_buf, &mut writer, tab); + super::write_nonprint_to_end(in_buf, &mut writer, tab).unwrap(); assert_eq!(writer.buffer(), tab); } @@ -767,7 +770,7 @@ mod tests { let mut writer = BufWriter::with_capacity(1024 * 64, stdout()); let in_buf = &[byte]; let tab = b""; - super::write_nonprint_to_end(in_buf, &mut writer, tab); + super::write_nonprint_to_end(in_buf, &mut writer, tab).unwrap(); assert_eq!(writer.buffer(), [b'^', byte + 64]); } } @@ -778,7 +781,7 @@ mod tests { let mut writer = BufWriter::with_capacity(1024 * 64, stdout()); let in_buf = &[byte]; let tab = b""; - super::write_nonprint_to_end(in_buf, &mut writer, tab); + super::write_nonprint_to_end(in_buf, &mut writer, tab).unwrap(); assert_eq!(writer.buffer(), [b'^', byte + 64]); } } From b1c662b89b4a64dbd532194d31ff008ba82b6dee Mon Sep 17 00:00:00 2001 From: oech3 <> Date: Sat, 3 Jan 2026 22:35:39 +0900 Subject: [PATCH 076/112] Bump libc to 0.2.178 with fix for FreeBSD --- Cargo.lock | 4 ++-- src/uucore/src/lib/features/fs.rs | 14 ++++---------- src/uucore/src/lib/features/fsext.rs | 5 +---- 3 files changed, 7 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 47ecd75af57..bbc4e73f0c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1651,9 +1651,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.175" +version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" [[package]] name = "libloading" diff --git a/src/uucore/src/lib/features/fs.rs b/src/uucore/src/lib/features/fs.rs index 9c710858082..bebfd1821cf 100644 --- a/src/uucore/src/lib/features/fs.rs +++ b/src/uucore/src/lib/features/fs.rs @@ -138,7 +138,6 @@ impl FileInformation { any( target_vendor = "apple", target_os = "android", - target_os = "freebsd", target_os = "netbsd", target_os = "openbsd", target_os = "illumos", @@ -152,6 +151,8 @@ impl FileInformation { ) ))] return self.0.st_nlink.into(); + #[cfg(target_os = "freebsd")] + return self.0.st_nlink; #[cfg(target_os = "aix")] return self.0.st_nlink.try_into().unwrap(); #[cfg(windows)] @@ -160,16 +161,9 @@ impl FileInformation { #[cfg(unix)] pub fn inode(&self) -> u64 { - #[cfg(all( - not(any(target_os = "freebsd", target_os = "netbsd")), - target_pointer_width = "64" - ))] + #[cfg(all(not(any(target_os = "netbsd")), target_pointer_width = "64"))] return self.0.st_ino; - #[cfg(any( - target_os = "freebsd", - target_os = "netbsd", - not(target_pointer_width = "64") - ))] + #[cfg(any(target_os = "netbsd", not(target_pointer_width = "64")))] return self.0.st_ino.into(); } } diff --git a/src/uucore/src/lib/features/fsext.rs b/src/uucore/src/lib/features/fsext.rs index f2ae59a76f3..ec88a5e614e 100644 --- a/src/uucore/src/lib/features/fsext.rs +++ b/src/uucore/src/lib/features/fsext.rs @@ -446,11 +446,8 @@ unsafe extern "C" { #[link_name = "getmntinfo"] fn get_mount_info(mount_buffer_p: *mut *mut StatFs, flags: c_int) -> c_int; - // Rust on FreeBSD uses 11.x ABI for filesystem metadata syscalls. - // Call the right version of the symbol for getmntinfo() result to - // match libc StatFS layout. #[cfg(target_os = "freebsd")] - #[link_name = "getmntinfo@FBSD_1.0"] + #[link_name = "getmntinfo"] fn get_mount_info(mount_buffer_p: *mut *mut StatFs, flags: c_int) -> c_int; } From 4645864d2e8693b8e9ca83d3e7bee4a5adc381ae Mon Sep 17 00:00:00 2001 From: CrazyRoka Date: Sun, 4 Jan 2026 12:34:42 +0000 Subject: [PATCH 077/112] shuf: optimize numeric output by avoiding write!() --- src/uu/shuf/src/shuf.rs | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/uu/shuf/src/shuf.rs b/src/uu/shuf/src/shuf.rs index e69ad1e1caf..4fd5ca85a0f 100644 --- a/src/uu/shuf/src/shuf.rs +++ b/src/uu/shuf/src/shuf.rs @@ -422,7 +422,26 @@ impl Writable for &OsStr { impl Writable for usize { fn write_all_to(&self, output: &mut impl OsWrite) -> Result<(), Error> { - write!(output, "{self}") + let mut n = *self; + + // Handle the zero case explicitly + if n == 0 { + return output.write_all(b"0"); + } + + // Maximum number of digits for u64 is 20 (18446744073709551615) + let mut buf = [0u8; 20]; + let mut i = 20; + + // Write digits from right to left + while n > 0 { + i -= 1; + buf[i] = b'0' + (n % 10) as u8; + n /= 10; + } + + // Write the relevant part of the buffer to output + output.write_all(&buf[i..]) } } From ab0a294d6bcf7f36576de4e1670325ff6366a5eb Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Sun, 4 Jan 2026 15:33:59 +0100 Subject: [PATCH 078/112] ci: set -no-metrics for Android emulator --- .github/workflows/android.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 93a9fec1e2f..a4a9b3bd08b 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -166,7 +166,7 @@ jobs: disk-size: ${{ env.EMULATOR_DISK_SIZE }} cores: ${{ env.EMULATOR_CORES }} force-avd-creation: false - emulator-options: ${{ env.COMMON_EMULATOR_OPTIONS }} -no-snapshot-save -snapshot ${{ env.AVD_CACHE_KEY }} + emulator-options: ${{ env.COMMON_EMULATOR_OPTIONS }} -no-metrics -no-snapshot-save -snapshot ${{ env.AVD_CACHE_KEY }} emulator-boot-timeout: ${{ env.EMULATOR_BOOT_TIMEOUT }} # This is not a usual script. Every line is executed in a separate shell with `sh -c`. If # one of the lines returns with error the whole script is failed (like running a script with From f10454b4c6919246986a3222d357690d5c7c37c6 Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Sun, 4 Jan 2026 16:59:27 +0100 Subject: [PATCH 079/112] clippy: allow "cygwin" as value for "target_os" --- Cargo.toml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6d34d5c1efe..6a5796a46c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -619,9 +619,12 @@ workspace = true # This is the linting configuration for all crates. # In order to use these, all crates have `[lints] workspace = true` section. [workspace.lints.rust] -# Allow "fuzzing" as a "cfg" condition name +# Allow "fuzzing" as a "cfg" condition name and "cygwin" as a value for "target_os" # https://doc.rust-lang.org/nightly/rustc/check-cfg/cargo-specifics.html -unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing)'] } +unexpected_cfgs = { level = "warn", check-cfg = [ + 'cfg(fuzzing)', + 'cfg(target_os, values("cygwin"))', +] } #unused_qualifications = "warn" // TODO: fix warnings in uucore, then re-enable this lint [workspace.lints.clippy] From 7583ea17215093efb4a686456b067c8aaf8a849d Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Sun, 4 Jan 2026 17:48:35 +0100 Subject: [PATCH 080/112] libstdbuf: enable workspace lints --- src/uu/stdbuf/src/libstdbuf/Cargo.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/uu/stdbuf/src/libstdbuf/Cargo.toml b/src/uu/stdbuf/src/libstdbuf/Cargo.toml index 6460c441eec..8a92fcbb56a 100644 --- a/src/uu/stdbuf/src/libstdbuf/Cargo.toml +++ b/src/uu/stdbuf/src/libstdbuf/Cargo.toml @@ -10,6 +10,9 @@ keywords.workspace = true categories.workspace = true edition.workspace = true +[lints] +workspace = true + [lib] name = "stdbuf" path = "src/libstdbuf.rs" From 1c3f2027149e3cf5f9e71b0c64aecc21a097a495 Mon Sep 17 00:00:00 2001 From: Jeffrey Finkelstein Date: Sun, 4 Jan 2026 18:09:57 -0500 Subject: [PATCH 081/112] pr: use 72 char line width for all page headers Set the default line width to 72 for all page headers in `pr`, regardless of whether a custom date format is being used. Before, a single space was used to separate the three components of the header (date, filename, and page number) if a custom date format was not given. --- src/uu/pr/src/pr.rs | 35 +- tests/by-util/test_pr.rs | 135 +++---- tests/fixtures/pr/0F | 10 +- tests/fixtures/pr/0Fnt-expected | 330 ++++++++++++++++++ tests/fixtures/pr/3-0F | 6 +- tests/fixtures/pr/3a3f-0F | 6 +- tests/fixtures/pr/3f-0F | 6 +- tests/fixtures/pr/a3-0F | 10 +- tests/fixtures/pr/a3f-0F | 10 +- tests/fixtures/pr/a3f-0Fnt-expected | 37 ++ tests/fixtures/pr/column.log.expected | 6 +- tests/fixtures/pr/column_across.log.expected | 6 +- .../pr/column_across_sep.log.expected | 6 +- .../pr/column_across_sep1.log.expected | 6 +- .../pr/column_spaces_across.log.expected | 6 +- tests/fixtures/pr/joined.log.expected | 4 +- tests/fixtures/pr/l24-FF | 26 +- tests/fixtures/pr/mpr.log.expected | 4 +- tests/fixtures/pr/mpr1.log.expected | 6 +- tests/fixtures/pr/mpr2.log.expected | 4 +- tests/fixtures/pr/stdin.log.expected | 4 +- .../fixtures/pr/test_num_page_2.log.expected | 4 +- .../pr/test_num_page_char.log.expected | 4 +- .../pr/test_num_page_char_one.log.expected | 4 +- tests/fixtures/pr/test_one_page.log.expected | 2 +- .../pr/test_one_page_double_line.log.expected | 4 +- .../pr/test_one_page_first_line.log.expected | 2 +- .../pr/test_one_page_header.log.expected | 2 +- .../fixtures/pr/test_page_length.log.expected | 4 +- .../pr/test_page_range_1.log.expected | 8 +- .../pr/test_page_range_2.log.expected | 6 +- 31 files changed, 532 insertions(+), 171 deletions(-) create mode 100644 tests/fixtures/pr/0Fnt-expected create mode 100644 tests/fixtures/pr/a3f-0Fnt-expected diff --git a/src/uu/pr/src/pr.rs b/src/uu/pr/src/pr.rs index fde2370480b..843b3b8f970 100644 --- a/src/uu/pr/src/pr.rs +++ b/src/uu/pr/src/pr.rs @@ -1180,34 +1180,23 @@ fn header_content(options: &OutputOptions, page: usize) -> Vec { // Use the line width if available, otherwise use default of 72 let total_width = options.line_width.unwrap_or(DEFAULT_COLUMN_WIDTH); - // GNU pr uses a specific layout: - // Date takes up the left part, filename is centered, page is right-aligned let date_len = date_part.chars().count(); let filename_len = filename.chars().count(); let page_len = page_part.chars().count(); let header_line = if date_len + filename_len + page_len + 2 < total_width { - // Check if we're using a custom date format that needs centered alignment - // This preserves backward compatibility while fixing the GNU time-style test - if date_part.starts_with('+') { - // GNU pr uses centered layout for headers with custom date formats - // The filename should be centered between the date and page parts - let space_for_filename = total_width - date_len - page_len; - let padding_before_filename = (space_for_filename - filename_len) / 2; - let padding_after_filename = - space_for_filename - filename_len - padding_before_filename; - - format!( - "{date_part}{:width1$}{filename}{:width2$}{page_part}", - "", - "", - width1 = padding_before_filename, - width2 = padding_after_filename - ) - } else { - // For standard date formats, use simple spacing for backward compatibility - format!("{date_part} {filename} {page_part}") - } + // The filename should be centered between the date and page parts + let space_for_filename = total_width - date_len - page_len; + let padding_before_filename = (space_for_filename - filename_len) / 2; + let padding_after_filename = space_for_filename - filename_len - padding_before_filename; + + format!( + "{date_part}{:width1$}{filename}{:width2$}{page_part}", + "", + "", + width1 = padding_before_filename, + width2 = padding_after_filename + ) } else { // If content is too long, just use single spaces format!("{date_part} {filename} {page_part}") diff --git a/tests/by-util/test_pr.rs b/tests/by-util/test_pr.rs index 26f64e1dc5e..63063a7e732 100644 --- a/tests/by-util/test_pr.rs +++ b/tests/by-util/test_pr.rs @@ -5,6 +5,7 @@ // spell-checker:ignore (ToDO) Sdivide use chrono::{DateTime, Duration, Utc}; +use regex::Regex; use std::fs::metadata; use uutests::new_ucmd; use uutests::util::UCommand; @@ -78,21 +79,22 @@ fn test_with_numbering_option_with_number_width() { #[test] fn test_with_long_header_option() { - let test_file_path = "test_one_page.log"; - let expected_test_file_path = "test_one_page_header.log.expected"; - let header = "new file"; - for args in [&["-h", header][..], &["--header=new file"][..]] { - let mut scenario = new_ucmd!(); - let value = file_last_modified_time(&scenario, test_file_path); - scenario - .args(args) - .arg(test_file_path) - .succeeds() - .stdout_is_templated_fixture( - expected_test_file_path, - &[("{last_modified_time}", &value), ("{header}", header)], - ); - } + let whitespace = " ".repeat(21); + let blank_lines = "\n".repeat(61); + let datetime_pattern = r"\d\d\d\d-\d\d-\d\d \d\d:\d\d"; + let pattern = + format!("\n\n{datetime_pattern}{whitespace}new file{whitespace}Page 1\n\n\na{blank_lines}"); + let regex = Regex::new(&pattern).unwrap(); + new_ucmd!() + .args(&["-h", "new file"]) + .pipe_in("a") + .succeeds() + .stdout_matches(®ex); + new_ucmd!() + .args(&["--header=new file"]) + .pipe_in("a") + .succeeds() + .stdout_matches(®ex); } #[test] @@ -400,99 +402,92 @@ fn test_with_offset_space_option() { #[test] fn test_with_date_format() { - let test_file_path = "test_one_page.log"; - let expected_test_file_path = "test_one_page.log.expected"; - let mut scenario = new_ucmd!(); - let value = file_last_modified_time_format(&scenario, test_file_path, "%Y__%s"); - scenario - .args(&[test_file_path, "-D", "%Y__%s"]) + let whitespace = " ".repeat(50); + let blank_lines = "\n".repeat(61); + let datetime_pattern = r"\d{4}__\d{10}"; + let pattern = format!("\n\n{datetime_pattern}{whitespace}Page 1\n\n\na{blank_lines}"); + let regex = Regex::new(&pattern).unwrap(); + new_ucmd!() + .args(&["-D", "%Y__%s"]) + .pipe_in("a") .succeeds() - .stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]); + .stdout_matches(®ex); // "Format" doesn't need to contain any replaceable token. + let whitespace = " ".repeat(60); + let blank_lines = "\n".repeat(61); new_ucmd!() - .args(&[test_file_path, "-D", "Hello!"]) + .args(&["-D", "Hello!"]) + .pipe_in("a") .succeeds() - .stdout_is_templated_fixture( - expected_test_file_path, - &[("{last_modified_time}", "Hello!")], - ); + .stdout_only(format!("\n\nHello!{whitespace}Page 1\n\n\na{blank_lines}")); // Long option also works new_ucmd!() - .args(&[test_file_path, "--date-format=Hello!"]) + .args(&["--date-format=Hello!"]) + .pipe_in("a") .succeeds() - .stdout_is_templated_fixture( - expected_test_file_path, - &[("{last_modified_time}", "Hello!")], - ); + .stdout_only(format!("\n\nHello!{whitespace}Page 1\n\n\na{blank_lines}")); // Option takes precedence over environment variables new_ucmd!() .env("POSIXLY_CORRECT", "1") .env("LC_TIME", "POSIX") - .args(&[test_file_path, "-D", "Hello!"]) + .args(&["--date-format=Hello!"]) + .pipe_in("a") .succeeds() - .stdout_is_templated_fixture( - expected_test_file_path, - &[("{last_modified_time}", "Hello!")], - ); + .stdout_only(format!("\n\nHello!{whitespace}Page 1\n\n\na{blank_lines}")); } #[test] fn test_with_date_format_env() { - const POSIXLY_FORMAT: &str = "%b %e %H:%M %Y"; - // POSIXLY_CORRECT + LC_ALL/TIME=POSIX uses "%b %e %H:%M %Y" date format - let test_file_path = "test_one_page.log"; - let expected_test_file_path = "test_one_page.log.expected"; - let mut scenario = new_ucmd!(); - let value = file_last_modified_time_format(&scenario, test_file_path, POSIXLY_FORMAT); - scenario + let whitespace = " ".repeat(49); + let blank_lines = "\n".repeat(61); + let datetime_pattern = r"[A-Z][a-z][a-z] [ \d]\d \d\d:\d\d \d{4}"; + let pattern = format!("\n\n{datetime_pattern}{whitespace}Page 1\n\n\na{blank_lines}"); + let regex = Regex::new(&pattern).unwrap(); + new_ucmd!() .env("POSIXLY_CORRECT", "1") .env("LC_ALL", "POSIX") - .args(&[test_file_path]) + .pipe_in("a") .succeeds() - .stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]); - - let mut scenario = new_ucmd!(); - let value = file_last_modified_time_format(&scenario, test_file_path, POSIXLY_FORMAT); - scenario + .stdout_matches(®ex); + new_ucmd!() .env("POSIXLY_CORRECT", "1") .env("LC_TIME", "POSIX") - .args(&[test_file_path]) + .pipe_in("a") .succeeds() - .stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]); + .stdout_matches(®ex); // But not if POSIXLY_CORRECT/LC_ALL is something else. - let mut scenario = new_ucmd!(); - let value = file_last_modified_time_format(&scenario, test_file_path, DATE_TIME_FORMAT_DEFAULT); - scenario + let whitespace = " ".repeat(50); + let datetime_pattern = r"\d\d\d\d-\d\d-\d\d \d\d:\d\d"; + let pattern = format!("\n\n{datetime_pattern}{whitespace}Page 1\n\n\na{blank_lines}"); + let regex = Regex::new(&pattern).unwrap(); + new_ucmd!() .env("LC_TIME", "POSIX") - .args(&[test_file_path]) + .pipe_in("a") .succeeds() - .stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]); - - let mut scenario = new_ucmd!(); - let value = file_last_modified_time_format(&scenario, test_file_path, DATE_TIME_FORMAT_DEFAULT); - scenario + .stdout_matches(®ex); + new_ucmd!() .env("POSIXLY_CORRECT", "1") .env("LC_TIME", "C") - .args(&[test_file_path]) + .pipe_in("a") .succeeds() - .stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]); + .stdout_matches(®ex); } #[test] fn test_with_pr_core_utils_tests() { let test_cases = vec![ ("", vec!["0Ft"], vec!["0F"], 0), - ("", vec!["0Fnt"], vec!["0F"], 0), + ("", vec!["0Fnt"], vec!["0Fnt-expected"], 0), ("+3", vec!["0Ft"], vec!["3-0F"], 0), ("+3 -f", vec!["0Ft"], vec!["3f-0F"], 0), ("-a -3", vec!["0Ft"], vec!["a3-0F"], 0), ("-a -3 -f", vec!["0Ft"], vec!["a3f-0F"], 0), - ("-a -3 -f", vec!["0Fnt"], vec!["a3f-0F"], 0), + ("-a -3 -f", vec!["0Fnt"], vec!["a3f-0Fnt-expected"], 0), ("+3 -a -3 -f", vec!["0Ft"], vec!["3a3f-0F"], 0), ("-l 24", vec!["FnFn"], vec!["l24-FF"], 0), ("-W 20 -l24 -f", vec!["tFFt-ll"], vec!["W20l24f-ll"], 0), @@ -622,3 +617,13 @@ fn test_b_flag_backwards_compat() { // -b is a no-op for backwards compatibility (column-down is now the default) new_ucmd!().args(&["-b", "-t"]).pipe_in("a\nb\n").succeeds(); } + +#[test] +fn test_page_header_width() { + let whitespace = " ".repeat(50); + let blank_lines = "\n".repeat(61); + let datetime_pattern = r"\d\d\d\d-\d\d-\d\d \d\d:\d\d"; + let pattern = format!("\n\n{datetime_pattern}{whitespace}Page 1\n\n\na{blank_lines}"); + let regex = Regex::new(&pattern).unwrap(); + new_ucmd!().pipe_in("a").succeeds().stdout_matches(®ex); +} diff --git a/tests/fixtures/pr/0F b/tests/fixtures/pr/0F index 2237653915c..af35676eabc 100644 --- a/tests/fixtures/pr/0F +++ b/tests/fixtures/pr/0F @@ -1,6 +1,6 @@ -{last_modified_time} {file_name} Page 1 +{last_modified_time} {file_name} Page 1 @@ -66,7 +66,7 @@ -{last_modified_time} {file_name} Page 2 +{last_modified_time} {file_name} Page 2 1 FF-Test: FF's at Start of File V @@ -132,7 +132,7 @@ -{last_modified_time} {file_name} Page 3 +{last_modified_time} {file_name} Page 3 @@ -198,7 +198,7 @@ -{last_modified_time} {file_name} Page 4 +{last_modified_time} {file_name} Page 4 15 xyzxyzxyz XYZXYZXYZ abcabcab @@ -264,7 +264,7 @@ -{last_modified_time} {file_name} Page 5 +{last_modified_time} {file_name} Page 5 29 xyzxyzxyz XYZXYZXYZ abcabcab diff --git a/tests/fixtures/pr/0Fnt-expected b/tests/fixtures/pr/0Fnt-expected new file mode 100644 index 00000000000..ab2f28a0987 --- /dev/null +++ b/tests/fixtures/pr/0Fnt-expected @@ -0,0 +1,330 @@ + + +{last_modified_time} {file_name} Page 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +{last_modified_time} {file_name} Page 2 + + +1 FF-Test: FF's at Start of File V +2 Options -b -3 / -a -3 / ... +3 -------------------------------------------- +4 3456789 123456789 123456789 123456789 12345678 +5 3 Columns downwards ..., <= 5 lines per page +6 FF-Arangements: Empty Pages at start +7 \ftext; \f\ntext; +8 \f\ftext; \f\f\ntext; \f\n\ftext; \f\n\f\n; +9 3456789 123456789 123456789 +10 zzzzzzzzzzzzzzzzzzzzzzzzzz123456789 +1 12345678 +2 12345678 +3 line truncation before FF; r_r_o_l-test: +14 456789 123456789 123456789 123456789 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +{last_modified_time} {file_name} Page 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +{last_modified_time} {file_name} Page 4 + + +15 xyzxyzxyz XYZXYZXYZ abcabcab +16 456789 123456789 xyzxyzxyz XYZXYZXYZ +7 12345678 +8 12345678 +9 3456789 ab +20 DEFGHI 123 +1 12345678 +2 12345678 +3 12345678 +4 12345678 +5 12345678 +6 12345678 +27 no truncation before FF; (r_l-test): +28 no trunc + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +{last_modified_time} {file_name} Page 5 + + +29 xyzxyzxyz XYZXYZXYZ abcabcab +30 456789 123456789 xyzxyzxyz XYZXYZXYZ +1 12345678 +2 3456789 abcdefghi +3 12345678 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/fixtures/pr/3-0F b/tests/fixtures/pr/3-0F index 25a9db1719e..3a9f0b657c6 100644 --- a/tests/fixtures/pr/3-0F +++ b/tests/fixtures/pr/3-0F @@ -1,6 +1,6 @@ -{last_modified_time} {file_name} Page 3 +{last_modified_time} {file_name} Page 3 @@ -66,7 +66,7 @@ -{last_modified_time} {file_name} Page 4 +{last_modified_time} {file_name} Page 4 15 xyzxyzxyz XYZXYZXYZ abcabcab @@ -132,7 +132,7 @@ -{last_modified_time} {file_name} Page 5 +{last_modified_time} {file_name} Page 5 29 xyzxyzxyz XYZXYZXYZ abcabcab diff --git a/tests/fixtures/pr/3a3f-0F b/tests/fixtures/pr/3a3f-0F index 6097374c776..f19823acc0a 100644 --- a/tests/fixtures/pr/3a3f-0F +++ b/tests/fixtures/pr/3a3f-0F @@ -1,11 +1,11 @@ -{last_modified_time} {file_name} Page 3 +{last_modified_time} {file_name} Page 3 -{last_modified_time} {file_name} Page 4 +{last_modified_time} {file_name} Page 4 15 xyzxyzxyz XYZXYZXYZ 16 456789 123456789 xyz 7 @@ -15,7 +15,7 @@ 27 no truncation before 28 no trunc -{last_modified_time} {file_name} Page 5 +{last_modified_time} {file_name} Page 5 29 xyzxyzxyz XYZXYZXYZ 30 456789 123456789 xyz 1 diff --git a/tests/fixtures/pr/3f-0F b/tests/fixtures/pr/3f-0F index d32c1f8f653..92805024aa2 100644 --- a/tests/fixtures/pr/3f-0F +++ b/tests/fixtures/pr/3f-0F @@ -1,11 +1,11 @@ -{last_modified_time} {file_name} Page 3 +{last_modified_time} {file_name} Page 3 -{last_modified_time} {file_name} Page 4 +{last_modified_time} {file_name} Page 4 15 xyzxyzxyz XYZXYZXYZ abcabcab @@ -25,7 +25,7 @@ -{last_modified_time} {file_name} Page 5 +{last_modified_time} {file_name} Page 5 29 xyzxyzxyz XYZXYZXYZ abcabcab diff --git a/tests/fixtures/pr/a3-0F b/tests/fixtures/pr/a3-0F index 58aeb07c2e4..302ab52d109 100644 --- a/tests/fixtures/pr/a3-0F +++ b/tests/fixtures/pr/a3-0F @@ -1,6 +1,6 @@ -{last_modified_time} {file_name} Page 1 +{last_modified_time} {file_name} Page 1 @@ -66,7 +66,7 @@ -{last_modified_time} {file_name} Page 2 +{last_modified_time} {file_name} Page 2 1 FF-Test: FF's at St 2 Options -b -3 / -a 3 ------------------- @@ -132,7 +132,7 @@ -{last_modified_time} {file_name} Page 3 +{last_modified_time} {file_name} Page 3 @@ -198,7 +198,7 @@ -{last_modified_time} {file_name} Page 4 +{last_modified_time} {file_name} Page 4 15 xyzxyzxyz XYZXYZXYZ 16 456789 123456789 xyz 7 @@ -264,7 +264,7 @@ -{last_modified_time} {file_name} Page 5 +{last_modified_time} {file_name} Page 5 29 xyzxyzxyz XYZXYZXYZ 30 456789 123456789 xyz 1 diff --git a/tests/fixtures/pr/a3f-0F b/tests/fixtures/pr/a3f-0F index 24939c004b0..54e0e80b5ac 100644 --- a/tests/fixtures/pr/a3f-0F +++ b/tests/fixtures/pr/a3f-0F @@ -1,11 +1,11 @@ -{last_modified_time} {file_name} Page 1 +{last_modified_time} {file_name} Page 1 -{last_modified_time} {file_name} Page 2 +{last_modified_time} {file_name} Page 2 1 FF-Test: FF's at St 2 Options -b -3 / -a 3 ------------------- @@ -15,12 +15,12 @@ 3 line truncation befor 14 456789 123456789 123 -{last_modified_time} {file_name} Page 3 +{last_modified_time} {file_name} Page 3 -{last_modified_time} {file_name} Page 4 +{last_modified_time} {file_name} Page 4 15 xyzxyzxyz XYZXYZXYZ 16 456789 123456789 xyz 7 @@ -30,7 +30,7 @@ 27 no truncation before 28 no trunc -{last_modified_time} {file_name} Page 5 +{last_modified_time} {file_name} Page 5 29 xyzxyzxyz XYZXYZXYZ 30 456789 123456789 xyz 1 diff --git a/tests/fixtures/pr/a3f-0Fnt-expected b/tests/fixtures/pr/a3f-0Fnt-expected new file mode 100644 index 00000000000..14d51325b04 --- /dev/null +++ b/tests/fixtures/pr/a3f-0Fnt-expected @@ -0,0 +1,37 @@ + + +{last_modified_time} {file_name} Page 1 + + + + +{last_modified_time} {file_name} Page 2 + + +1 FF-Test: FF's at St 2 Options -b -3 / -a 3 ------------------- +4 3456789 123456789 123 5 3 Columns downwards 6 FF-Arangements: Emp +7 \ftext; \f\ntext; 8 \f\ftext; \f\f\ntex 9 3456789 123456789 123 +10 zzzzzzzzzzzzzzzzzzz 1 2 +3 line truncation befor 14 456789 123456789 123 + + +{last_modified_time} {file_name} Page 3 + + + + +{last_modified_time} {file_name} Page 4 + + +15 xyzxyzxyz XYZXYZXYZ 16 456789 123456789 xyz 7 +8 9 3456789 ab 20 DEFGHI 123 +1 2 3 +4 5 6 +27 no truncation before 28 no trunc + + +{last_modified_time} {file_name} Page 5 + + +29 xyzxyzxyz XYZXYZXYZ 30 456789 123456789 xyz 1 +2 3456789 abcdefghi 3 \ No newline at end of file diff --git a/tests/fixtures/pr/column.log.expected b/tests/fixtures/pr/column.log.expected index e548d41284f..6e817eced33 100644 --- a/tests/fixtures/pr/column.log.expected +++ b/tests/fixtures/pr/column.log.expected @@ -1,6 +1,6 @@ -{last_modified_time} column.log Page 3 +{last_modified_time} column.log Page 3 337 337 393 393 449 449 @@ -66,7 +66,7 @@ -{last_modified_time} column.log Page 4 +{last_modified_time} column.log Page 4 505 505 561 561 617 617 @@ -132,7 +132,7 @@ -{last_modified_time} column.log Page 5 +{last_modified_time} column.log Page 5 673 673 729 729 785 785 diff --git a/tests/fixtures/pr/column_across.log.expected b/tests/fixtures/pr/column_across.log.expected index 9d5a1dc1ca4..4b0c93856b8 100644 --- a/tests/fixtures/pr/column_across.log.expected +++ b/tests/fixtures/pr/column_across.log.expected @@ -1,6 +1,6 @@ -{last_modified_time} column.log Page 3 +{last_modified_time} column.log Page 3 337 337 338 338 339 339 @@ -66,7 +66,7 @@ -{last_modified_time} column.log Page 4 +{last_modified_time} column.log Page 4 505 505 506 506 507 507 @@ -132,7 +132,7 @@ -{last_modified_time} column.log Page 5 +{last_modified_time} column.log Page 5 673 673 674 674 675 675 diff --git a/tests/fixtures/pr/column_across_sep.log.expected b/tests/fixtures/pr/column_across_sep.log.expected index 65c3e71c8ff..aad7dff2750 100644 --- a/tests/fixtures/pr/column_across_sep.log.expected +++ b/tests/fixtures/pr/column_across_sep.log.expected @@ -1,6 +1,6 @@ -{last_modified_time} column.log Page 3 +{last_modified_time} column.log Page 3 337 337 | 338 338 | 339 339 @@ -66,7 +66,7 @@ -{last_modified_time} column.log Page 4 +{last_modified_time} column.log Page 4 505 505 | 506 506 | 507 507 @@ -132,7 +132,7 @@ -{last_modified_time} column.log Page 5 +{last_modified_time} column.log Page 5 673 673 | 674 674 | 675 675 diff --git a/tests/fixtures/pr/column_across_sep1.log.expected b/tests/fixtures/pr/column_across_sep1.log.expected index f9dd454d708..e28885a4e35 100644 --- a/tests/fixtures/pr/column_across_sep1.log.expected +++ b/tests/fixtures/pr/column_across_sep1.log.expected @@ -1,6 +1,6 @@ -{last_modified_time} column.log Page 3 +{last_modified_time} column.log Page 3 337 337 divide 338 338 divide 339 339 @@ -66,7 +66,7 @@ -{last_modified_time} column.log Page 4 +{last_modified_time} column.log Page 4 505 505 divide 506 506 divide 507 507 @@ -132,7 +132,7 @@ -{last_modified_time} column.log Page 5 +{last_modified_time} column.log Page 5 673 673 divide 674 674 divide 675 675 diff --git a/tests/fixtures/pr/column_spaces_across.log.expected b/tests/fixtures/pr/column_spaces_across.log.expected index 037dd814ba3..77303249bfc 100644 --- a/tests/fixtures/pr/column_spaces_across.log.expected +++ b/tests/fixtures/pr/column_spaces_across.log.expected @@ -1,6 +1,6 @@ -{last_modified_time} column.log Page 3 +{last_modified_time} column.log Page 3 337 337 338 338 339 339 @@ -66,7 +66,7 @@ -{last_modified_time} column.log Page 4 +{last_modified_time} column.log Page 4 505 505 506 506 507 507 @@ -132,7 +132,7 @@ -{last_modified_time} column.log Page 5 +{last_modified_time} column.log Page 5 673 673 674 674 675 675 diff --git a/tests/fixtures/pr/joined.log.expected b/tests/fixtures/pr/joined.log.expected index a9cee6e4f4b..4176944a6f8 100644 --- a/tests/fixtures/pr/joined.log.expected +++ b/tests/fixtures/pr/joined.log.expected @@ -1,6 +1,6 @@ -{last_modified_time} Page 1 +{last_modified_time} Page 1 ##ntation processAirPortStateChanges]: pppConnectionState 0 @@ -66,7 +66,7 @@ Mon Dec 10 11:42:59.352 Info: 802.1X changed -{last_modified_time} Page 2 +{last_modified_time} Page 2 Mon Dec 10 11:42:59.354 Info: -[AirPortExtraImplementation processAirPortStateChanges]: pppConnectionState 0 diff --git a/tests/fixtures/pr/l24-FF b/tests/fixtures/pr/l24-FF index de219b2fb1f..2da241047cb 100644 --- a/tests/fixtures/pr/l24-FF +++ b/tests/fixtures/pr/l24-FF @@ -1,6 +1,6 @@ -{last_modified_time} {file_name} Page 1 +{last_modified_time} {file_name} Page 1 1 FF-Test: FF's in Text V @@ -24,7 +24,7 @@ -{last_modified_time} {file_name} Page 2 +{last_modified_time} {file_name} Page 2 @@ -48,7 +48,7 @@ -{last_modified_time} {file_name} Page 3 +{last_modified_time} {file_name} Page 3 @@ -72,7 +72,7 @@ -{last_modified_time} {file_name} Page 4 +{last_modified_time} {file_name} Page 4 15 xyzxyzxyz XYZXYZXYZ abcabcab @@ -96,7 +96,7 @@ -{last_modified_time} {file_name} Page 5 +{last_modified_time} {file_name} Page 5 @@ -120,7 +120,7 @@ -{last_modified_time} {file_name} Page 6 +{last_modified_time} {file_name} Page 6 @@ -144,7 +144,7 @@ -{last_modified_time} {file_name} Page 7 +{last_modified_time} {file_name} Page 7 29 xyzxyzxyz XYZXYZXYZ abcabcab @@ -168,7 +168,7 @@ -{last_modified_time} {file_name} Page 8 +{last_modified_time} {file_name} Page 8 @@ -192,7 +192,7 @@ -{last_modified_time} {file_name} Page 9 +{last_modified_time} {file_name} Page 9 @@ -216,7 +216,7 @@ -{last_modified_time} {file_name} Page 10 +{last_modified_time} {file_name} Page 10 @@ -240,7 +240,7 @@ -{last_modified_time} {file_name} Page 11 +{last_modified_time} {file_name} Page 11 43 xyzxyzxyz XYZXYZXYZ abcabcab @@ -264,7 +264,7 @@ -{last_modified_time} {file_name} Page 12 +{last_modified_time} {file_name} Page 12 @@ -288,7 +288,7 @@ -{last_modified_time} {file_name} Page 13 +{last_modified_time} {file_name} Page 13 57 xyzxyzxyz XYZXYZXYZ abcabcab diff --git a/tests/fixtures/pr/mpr.log.expected b/tests/fixtures/pr/mpr.log.expected index f6fffd19141..0f4d276b108 100644 --- a/tests/fixtures/pr/mpr.log.expected +++ b/tests/fixtures/pr/mpr.log.expected @@ -1,6 +1,6 @@ -{last_modified_time} Page 1 +{last_modified_time} Page 1 1 1 ## @@ -66,7 +66,7 @@ -{last_modified_time} Page 2 +{last_modified_time} Page 2 57 57 diff --git a/tests/fixtures/pr/mpr1.log.expected b/tests/fixtures/pr/mpr1.log.expected index 64d786d90ad..1d691599878 100644 --- a/tests/fixtures/pr/mpr1.log.expected +++ b/tests/fixtures/pr/mpr1.log.expected @@ -1,6 +1,6 @@ -{last_modified_time} Page 2 +{last_modified_time} Page 2 57 57 @@ -66,7 +66,7 @@ -{last_modified_time} Page 3 +{last_modified_time} Page 3 113 113 @@ -132,7 +132,7 @@ -{last_modified_time} Page 4 +{last_modified_time} Page 4 169 169 diff --git a/tests/fixtures/pr/mpr2.log.expected b/tests/fixtures/pr/mpr2.log.expected index 091f0f2280f..9c453924ccc 100644 --- a/tests/fixtures/pr/mpr2.log.expected +++ b/tests/fixtures/pr/mpr2.log.expected @@ -1,6 +1,6 @@ -{last_modified_time} Page 1 +{last_modified_time} Page 1 1 1 ## 1 @@ -100,7 +100,7 @@ -{last_modified_time} Page 2 +{last_modified_time} Page 2 91 91 91 diff --git a/tests/fixtures/pr/stdin.log.expected b/tests/fixtures/pr/stdin.log.expected index 6922ee59454..5f9d6c23535 100644 --- a/tests/fixtures/pr/stdin.log.expected +++ b/tests/fixtures/pr/stdin.log.expected @@ -1,6 +1,6 @@ -{last_modified_time} Page 1 +{last_modified_time} Page 1 1 ntation processAirPortStateChanges]: pppConnectionState 0 @@ -66,7 +66,7 @@ -{last_modified_time} Page 2 +{last_modified_time} Page 2 57 Mon Dec 10 11:42:59.354 Info: -[AirPortExtraImplementation processAirPortStateChanges]: pppConnectionState 0 diff --git a/tests/fixtures/pr/test_num_page_2.log.expected b/tests/fixtures/pr/test_num_page_2.log.expected index dae437ef867..bf9a6c174fd 100644 --- a/tests/fixtures/pr/test_num_page_2.log.expected +++ b/tests/fixtures/pr/test_num_page_2.log.expected @@ -1,6 +1,6 @@ -{last_modified_time} test_num_page.log Page 1 +{last_modified_time} test_num_page.log Page 1 1 ntation processAirPortStateChanges]: pppConnectionState 0 @@ -66,7 +66,7 @@ -{last_modified_time} test_num_page.log Page 2 +{last_modified_time} test_num_page.log Page 2 57 ntation processAirPortStateChanges]: pppConnectionState 0 diff --git a/tests/fixtures/pr/test_num_page_char.log.expected b/tests/fixtures/pr/test_num_page_char.log.expected index 169dbd844d2..0536b75c0d8 100644 --- a/tests/fixtures/pr/test_num_page_char.log.expected +++ b/tests/fixtures/pr/test_num_page_char.log.expected @@ -1,6 +1,6 @@ -{last_modified_time} test_num_page.log Page 1 +{last_modified_time} test_num_page.log Page 1 1cntation processAirPortStateChanges]: pppConnectionState 0 @@ -66,7 +66,7 @@ -{last_modified_time} test_num_page.log Page 2 +{last_modified_time} test_num_page.log Page 2 57cntation processAirPortStateChanges]: pppConnectionState 0 diff --git a/tests/fixtures/pr/test_num_page_char_one.log.expected b/tests/fixtures/pr/test_num_page_char_one.log.expected index dd78131921e..cd0b1278115 100644 --- a/tests/fixtures/pr/test_num_page_char_one.log.expected +++ b/tests/fixtures/pr/test_num_page_char_one.log.expected @@ -1,6 +1,6 @@ -{last_modified_time} test_num_page.log Page 1 +{last_modified_time} test_num_page.log Page 1 1cntation processAirPortStateChanges]: pppConnectionState 0 @@ -66,7 +66,7 @@ -{last_modified_time} test_num_page.log Page 2 +{last_modified_time} test_num_page.log Page 2 7cntation processAirPortStateChanges]: pppConnectionState 0 diff --git a/tests/fixtures/pr/test_one_page.log.expected b/tests/fixtures/pr/test_one_page.log.expected index 54f7723924f..fc354b41d84 100644 --- a/tests/fixtures/pr/test_one_page.log.expected +++ b/tests/fixtures/pr/test_one_page.log.expected @@ -1,6 +1,6 @@ -{last_modified_time} test_one_page.log Page 1 +{last_modified_time} test_one_page.log Page 1 ntation processAirPortStateChanges]: pppConnectionState 0 diff --git a/tests/fixtures/pr/test_one_page_double_line.log.expected b/tests/fixtures/pr/test_one_page_double_line.log.expected index e32101fcf5d..49ed90c8752 100644 --- a/tests/fixtures/pr/test_one_page_double_line.log.expected +++ b/tests/fixtures/pr/test_one_page_double_line.log.expected @@ -1,6 +1,6 @@ -{last_modified_time} test_one_page.log Page 1 +{last_modified_time} test_one_page.log Page 1 ntation processAirPortStateChanges]: pppConnectionState 0 @@ -66,7 +66,7 @@ Mon Dec 10 11:42:57.751 Info: -[AirPortExtraImplementati -{last_modified_time} test_one_page.log Page 2 +{last_modified_time} test_one_page.log Page 2 Mon Dec 10 11:42:57.896 Info: 802.1X changed diff --git a/tests/fixtures/pr/test_one_page_first_line.log.expected b/tests/fixtures/pr/test_one_page_first_line.log.expected index 303f01c732b..5c7b2eebe66 100644 --- a/tests/fixtures/pr/test_one_page_first_line.log.expected +++ b/tests/fixtures/pr/test_one_page_first_line.log.expected @@ -1,6 +1,6 @@ -{last_modified_time} test_one_page.log Page 1 +{last_modified_time} test_one_page.log Page 1 5 ntation processAirPortStateChanges]: pppConnectionState 0 diff --git a/tests/fixtures/pr/test_one_page_header.log.expected b/tests/fixtures/pr/test_one_page_header.log.expected index a00d5f85505..06a69088c25 100644 --- a/tests/fixtures/pr/test_one_page_header.log.expected +++ b/tests/fixtures/pr/test_one_page_header.log.expected @@ -1,6 +1,6 @@ -{last_modified_time} {header} Page 1 +{last_modified_time} {header} Page 1 ntation processAirPortStateChanges]: pppConnectionState 0 diff --git a/tests/fixtures/pr/test_page_length.log.expected b/tests/fixtures/pr/test_page_length.log.expected index 8f4ab82d1cd..38578c1dcaa 100644 --- a/tests/fixtures/pr/test_page_length.log.expected +++ b/tests/fixtures/pr/test_page_length.log.expected @@ -1,6 +1,6 @@ -{last_modified_time} test.log Page 2 +{last_modified_time} test.log Page 2 91 Mon Dec 10 11:43:31.748 )} took 0.0025 seconds, returned 10 results @@ -100,7 +100,7 @@ -{last_modified_time} test.log Page 3 +{last_modified_time} test.log Page 3 181 Mon Dec 10 11:52:32.715 AutoJoin: Successful cache-assisted scan request for locationd with channels {( diff --git a/tests/fixtures/pr/test_page_range_1.log.expected b/tests/fixtures/pr/test_page_range_1.log.expected index f254261d4bc..fa35f844509 100644 --- a/tests/fixtures/pr/test_page_range_1.log.expected +++ b/tests/fixtures/pr/test_page_range_1.log.expected @@ -1,6 +1,6 @@ -{last_modified_time} test.log Page 15 +{last_modified_time} test.log Page 15 Mon Dec 10 12:05:48.183 [channelNumber=12(2GHz), channelWidth={20MHz}, active] @@ -66,7 +66,7 @@ Mon Dec 10 12:06:28.765 Roam: ROAMING PROFILES updated to SINGLE -{last_modified_time} test.log Page 16 +{last_modified_time} test.log Page 16 Mon Dec 10 12:06:28.770 SC: airportdProcessSystemConfigurationEvent: Processing 'State:/Network/Interface/en0/AirPort/ProfileID' @@ -132,7 +132,7 @@ Mon Dec 10 12:06:50.945 BTC: __BluetoothCoexHandleUpdateForNode: -{last_modified_time} test.log Page 17 +{last_modified_time} test.log Page 17 Mon Dec 10 12:06:50.945 BTC: BluetoothCoexSetProfile: profile for band 2.4GHz didn't change @@ -198,7 +198,7 @@ Mon Dec 10 12:13:27.640 Info: link quality changed -{last_modified_time} test.log Page 18 +{last_modified_time} test.log Page 18 Mon Dec 10 12:14:46.658 Info: SCAN request received from pid 92 (locationd) with priority 2 diff --git a/tests/fixtures/pr/test_page_range_2.log.expected b/tests/fixtures/pr/test_page_range_2.log.expected index 4f260eb6544..2ca5ed04dca 100644 --- a/tests/fixtures/pr/test_page_range_2.log.expected +++ b/tests/fixtures/pr/test_page_range_2.log.expected @@ -1,6 +1,6 @@ -{last_modified_time} test.log Page 15 +{last_modified_time} test.log Page 15 Mon Dec 10 12:05:48.183 [channelNumber=12(2GHz), channelWidth={20MHz}, active] @@ -66,7 +66,7 @@ Mon Dec 10 12:06:28.765 Roam: ROAMING PROFILES updated to SINGLE -{last_modified_time} test.log Page 16 +{last_modified_time} test.log Page 16 Mon Dec 10 12:06:28.770 SC: airportdProcessSystemConfigurationEvent: Processing 'State:/Network/Interface/en0/AirPort/ProfileID' @@ -132,7 +132,7 @@ Mon Dec 10 12:06:50.945 BTC: __BluetoothCoexHandleUpdateForNode: -{last_modified_time} test.log Page 17 +{last_modified_time} test.log Page 17 Mon Dec 10 12:06:50.945 BTC: BluetoothCoexSetProfile: profile for band 2.4GHz didn't change From b50288d9cf17b6b5e5daa50ebba735bee83e157a Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Mon, 5 Jan 2026 04:39:45 +0900 Subject: [PATCH 082/112] cksum.rs: Simple default tag variable --- src/uu/cksum/src/cksum.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uu/cksum/src/cksum.rs b/src/uu/cksum/src/cksum.rs index 3d814ae6f24..70c80ae37c7 100644 --- a/src/uu/cksum/src/cksum.rs +++ b/src/uu/cksum/src/cksum.rs @@ -164,7 +164,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // Set the default algorithm to CRC when not '--check'ing. let algo_kind = algo_cli.unwrap_or(AlgoKind::Crc); - let tag = matches.get_flag(options::TAG) || !matches.get_flag(options::UNTAGGED); + let tag = !matches.get_flag(options::UNTAGGED); // Making TAG default at clap blocks --untagged let binary = matches.get_flag(options::BINARY); let algo = SizedAlgoKind::from_unsized(algo_kind, length)?; From c20f7962d7c486de9abd63fb1080cfa473f2a757 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Mon, 5 Jan 2026 04:22:00 +0900 Subject: [PATCH 083/112] cksum,hashsum: Drop a message replaced by clap --- src/uucore/src/lib/features/checksum/mod.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/uucore/src/lib/features/checksum/mod.rs b/src/uucore/src/lib/features/checksum/mod.rs index 2f3d28b4121..7cf7fe129fa 100644 --- a/src/uucore/src/lib/features/checksum/mod.rs +++ b/src/uucore/src/lib/features/checksum/mod.rs @@ -374,9 +374,6 @@ pub enum ChecksumError { #[error("the --raw option is not supported with multiple files")] RawMultipleFiles, - #[error("the --{0} option is meaningful only when verifying checksums")] - CheckOnlyFlag(String), - // --length sanitization errors #[error("--length required for {}", .0.quote())] LengthRequired(String), From b639fe45b3a317e6eedc0b991a6b8534a3a78c95 Mon Sep 17 00:00:00 2001 From: oech3 <> Date: Mon, 5 Jan 2026 16:54:18 +0900 Subject: [PATCH 084/112] build-gnu.sh: Let md5sum.pl clap compatible --- util/build-gnu.sh | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/util/build-gnu.sh b/util/build-gnu.sh index 2ae6b1e6186..421b43d5e7e 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -320,11 +320,9 @@ echo "n_stat1 = \$n_stat1"\n\ echo "n_stat2 = \$n_stat2"\n\ test \$n_stat1 -ge \$n_stat2 \\' tests/ls/stat-free-color.sh -# no need to replicate this output with hashsum +# clap changes the error message. Check exit code only. "${SED}" -i -e "s|Try 'md5sum --help' for more information.\\\n||" tests/cksum/md5sum.pl -# clap changes the error message - "${SED}" -i '/check-ignore-missing-4/,/EXIT=> 1/ { /ERR=>/,/try_help/d }' tests/cksum/md5sum.pl - +"${SED}" -i '/check-ignore-missing-4/,/EXIT/c \ ['\''check-ignore-missing-4'\'', '\''--ignore-missing'\'', {IN=> {f=> '\'''\''}}, {ERR_SUBST=>"s/.*//s"}, {EXIT=> 1}],' tests/cksum/md5sum.pl # Our ls command always outputs ANSI color codes prepended with a zero. However, # in the case of GNU, it seems inconsistent. Nevertheless, it looks like it # doesn't matter whether we prepend a zero or not. From b264ac8ed638225a03ab1396baea748b9397a7ad Mon Sep 17 00:00:00 2001 From: CrazyRoka Date: Sun, 4 Jan 2026 13:42:07 +0000 Subject: [PATCH 085/112] uniq: optimize memory usage for ignore-case comparison --- src/uu/uniq/src/uniq.rs | 29 +---------------------------- 1 file changed, 1 insertion(+), 28 deletions(-) diff --git a/src/uu/uniq/src/uniq.rs b/src/uu/uniq/src/uniq.rs index 3845ba459ea..9c95305e4be 100644 --- a/src/uu/uniq/src/uniq.rs +++ b/src/uu/uniq/src/uniq.rs @@ -61,8 +61,6 @@ struct Uniq { struct LineMeta { key_start: usize, key_end: usize, - lowercase: Vec, - use_lowercase: bool, } macro_rules! write_line_terminator { @@ -152,18 +150,7 @@ impl Uniq { return first_slice != second_slice; } - let first_cmp = if first_meta.use_lowercase { - first_meta.lowercase.as_slice() - } else { - first_slice - }; - let second_cmp = if second_meta.use_lowercase { - second_meta.lowercase.as_slice() - } else { - second_slice - }; - - first_cmp != second_cmp + !first_slice.eq_ignore_ascii_case(second_slice) } fn key_bounds(&self, line: &[u8]) -> (usize, usize) { @@ -230,20 +217,6 @@ impl Uniq { let (key_start, key_end) = self.key_bounds(line); meta.key_start = key_start; meta.key_end = key_end; - - if self.ignore_case && key_start < key_end { - let slice = &line[key_start..key_end]; - if slice.iter().any(|b| b.is_ascii_uppercase()) { - meta.lowercase.clear(); - meta.lowercase.reserve(slice.len()); - meta.lowercase - .extend(slice.iter().map(|b| b.to_ascii_lowercase())); - meta.use_lowercase = true; - return; - } - } - - meta.use_lowercase = false; } fn read_line( From 94047cd9a158bf0de58fe0879702e26309468207 Mon Sep 17 00:00:00 2001 From: max-amb Date: Sun, 4 Jan 2026 13:18:48 +0000 Subject: [PATCH 086/112] cp: Added test for permissions copying to an existing file --- tests/by-util/test_cp.rs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index 5f4a44c4aff..f0ad9d8ca38 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -7444,3 +7444,28 @@ fn test_cp_archive_deref_flag_ordering() { assert_eq!(at.is_symlink(&dest), expect_symlink, "failed for {flags}"); } } + +/// Test that copying to an existing file maintains its permissions, unix only because .mode() only +/// works on Unix +#[test] +#[cfg(unix)] +fn test_cp_to_existing_file_permissions() { + let (at, mut ucmd) = at_and_ucmd!(); + + at.touch("src"); + at.touch("dst"); + + let src_path = at.plus("src"); + let dst_path = at.plus("dst"); + + let mut src_permissions = std::fs::metadata(&src_path).unwrap().permissions(); + src_permissions.set_readonly(true); + std::fs::set_permissions(&src_path, src_permissions).unwrap(); + + let dst_mode = std::fs::metadata(&dst_path).unwrap().permissions().mode(); + + ucmd.args(&["src", "dst"]).succeeds(); + + let new_dst_mode = std::fs::metadata(&dst_path).unwrap().permissions().mode(); + assert_eq!(dst_mode, new_dst_mode); +} From ebee23184420f3ceead25ae4715ebaf3ee9054e0 Mon Sep 17 00:00:00 2001 From: Chris Dryden Date: Mon, 5 Jan 2026 12:02:37 -0500 Subject: [PATCH 087/112] dd: fix nocache flag handling at EOF (#9818) * dd: fix nocache flag handling at EOF * Add tests for nocache EOF handling --------- Co-authored-by: Sylvestre Ledru --- src/uu/dd/src/dd.rs | 44 ++++++++-------------------------- tests/by-util/test_dd.rs | 51 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 60 insertions(+), 35 deletions(-) diff --git a/src/uu/dd/src/dd.rs b/src/uu/dd/src/dd.rs index 412b6668fe9..567f803d390 100644 --- a/src/uu/dd/src/dd.rs +++ b/src/uu/dd/src/dd.rs @@ -219,17 +219,6 @@ impl Source { Self::StdinFile(f) } - /// The length of the data source in number of bytes. - /// - /// If it cannot be determined, then this function returns 0. - fn len(&self) -> io::Result { - #[allow(clippy::match_wildcard_for_single_variants)] - match self { - Self::File(f) => Ok(f.metadata()?.len().try_into().unwrap_or(i64::MAX)), - _ => Ok(0), - } - } - fn skip(&mut self, n: u64) -> io::Result { match self { #[cfg(not(unix))] @@ -673,17 +662,6 @@ impl Dest { _ => Err(Errno::ESPIPE), // "Illegal seek" } } - - /// The length of the data destination in number of bytes. - /// - /// If it cannot be determined, then this function returns 0. - fn len(&self) -> io::Result { - #[allow(clippy::match_wildcard_for_single_variants)] - match self { - Self::File(f, _) => Ok(f.metadata()?.len().try_into().unwrap_or(i64::MAX)), - _ => Ok(0), - } - } } /// Decide whether the given buffer is all zeros. @@ -1063,21 +1041,12 @@ impl BlockWriter<'_> { /// depending on the command line arguments, this function /// informs the OS to flush/discard the caches for input and/or output file. fn flush_caches_full_length(i: &Input, o: &Output) -> io::Result<()> { - // TODO Better error handling for overflowing `len`. + // Using len=0 in posix_fadvise means "to end of file" if i.settings.iflags.nocache { - let offset = 0; - #[allow(clippy::useless_conversion)] - let len = i.src.len()?.try_into().unwrap(); - i.discard_cache(offset, len); + i.discard_cache(0, 0); } - // Similarly, discard the system cache for the output file. - // - // TODO Better error handling for overflowing `len`. if i.settings.oflags.nocache { - let offset = 0; - #[allow(clippy::useless_conversion)] - let len = o.dst.len()?.try_into().unwrap(); - o.discard_cache(offset, len); + o.discard_cache(0, 0); } Ok(()) @@ -1185,6 +1154,7 @@ fn dd_copy(mut i: Input, o: Output) -> io::Result<()> { let input_nocache = i.settings.iflags.nocache; let output_nocache = o.settings.oflags.nocache; + let output_direct = o.settings.oflags.direct; // Add partial block buffering, if needed. let mut o = if o.settings.buffered { @@ -1208,6 +1178,12 @@ fn dd_copy(mut i: Input, o: Output) -> io::Result<()> { let loop_bsize = calc_loop_bsize(i.settings.count, &rstat, &wstat, i.settings.ibs, bsize); let rstat_update = read_helper(&mut i, &mut buf, loop_bsize)?; if rstat_update.is_empty() { + if input_nocache { + i.discard_cache(read_offset.try_into().unwrap(), 0); + } + if output_nocache || output_direct { + o.discard_cache(write_offset.try_into().unwrap(), 0); + } break; } let wstat_update = o.write_blocks(&buf)?; diff --git a/tests/by-util/test_dd.rs b/tests/by-util/test_dd.rs index a6a52e66fb5..35a1561e498 100644 --- a/tests/by-util/test_dd.rs +++ b/tests/by-util/test_dd.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 fname, tname, fpath, specfile, testfile, unspec, ifile, ofile, outfile, fullblock, urand, fileio, atoe, atoibm, availible, behaviour, bmax, bremain, btotal, cflags, creat, ctable, ctty, datastructures, doesnt, etoa, fileout, fname, gnudd, iconvflags, iseek, nocache, noctty, noerror, nofollow, nolinks, nonblock, oconvflags, oseek, outfile, parseargs, rlen, rmax, rposition, rremain, rsofar, rstat, sigusr, sigval, wlen, wstat abcdefghijklm abcdefghi nabcde nabcdefg abcdefg fifoname seekable +// spell-checker:ignore fname, tname, fpath, specfile, testfile, unspec, ifile, ofile, outfile, fullblock, urand, fileio, atoe, atoibm, availible, behaviour, bmax, bremain, btotal, cflags, creat, ctable, ctty, datastructures, doesnt, etoa, fileout, fname, gnudd, iconvflags, iseek, nocache, noctty, noerror, nofollow, nolinks, nonblock, oconvflags, oseek, outfile, parseargs, rlen, rmax, rposition, rremain, rsofar, rstat, sigusr, sigval, wlen, wstat abcdefghijklm abcdefghi nabcde nabcdefg abcdefg fifoname seekable fadvise FADV DONTNEED use uutests::at_and_ucmd; use uutests::new_ucmd; @@ -1840,3 +1840,52 @@ fn test_skip_overflow() { "dd: invalid number: ‘9223372036854775808’: Value too large for defined data type", ); } + +#[test] +#[cfg(target_os = "linux")] +fn test_nocache_eof() { + let (at, mut ucmd) = at_and_ucmd!(); + at.write_bytes("in.f", &vec![0u8; 1234567]); + ucmd.args(&[ + "if=in.f", + "of=out.f", + "bs=1M", + "oflag=nocache,sync", + "status=noxfer", + ]) + .succeeds(); + assert_eq!(at.read_bytes("out.f").len(), 1234567); +} + +#[test] +#[cfg(all(target_os = "linux", feature = "printf"))] +fn test_nocache_eof_fadvise_zero_length() { + use std::process::Command; + let (at, _ucmd) = at_and_ucmd!(); + at.write_bytes("in.f", &vec![0u8; 1234567]); + + let strace_file = at.plus_as_string("strace.out"); + let result = Command::new("strace") + .args(["-o", &strace_file, "-e", "fadvise64,fadvise64_64"]) + .arg(get_tests_binary()) + .args([ + "dd", + "if=in.f", + "of=out.f", + "bs=1M", + "oflag=nocache,sync", + "status=none", + ]) + .current_dir(at.as_string()) + .output(); + + if result.is_err() { + return; // strace not available + } + + let strace = at.read("strace.out"); + assert!( + strace.contains(", 0, POSIX_FADV_DONTNEED"), + "Expected len=0 at EOF: {strace}" + ); +} From 4a48691c3e833a3893a1da7653ba9c6c571b357f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 6 Jan 2026 01:58:32 +0000 Subject: [PATCH 088/112] chore(deps): update rust crate jiff to v0.2.18 --- Cargo.lock | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bbc4e73f0c6..3697d1dbdbd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1565,9 +1565,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a87d9b8105c23642f50cbbae03d1f75d8422c5cb98ce7ee9271f7ff7505be6b8" +checksum = "e67e8da4c49d6d9909fe03361f9b620f58898859f5c7aded68351e85e71ecf50" dependencies = [ "jiff-static", "jiff-tzdb-platform", @@ -1575,14 +1575,14 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde_core", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] name = "jiff-static" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b787bebb543f8969132630c51fd0afab173a86c6abae56ff3b9e5e3e3f9f6e58" +checksum = "e0c84ee7f197eca9a86c6fd6cb771e55eb991632f15f2bc3ca6ec838929e6e78" dependencies = [ "proc-macro2", "quote", @@ -1873,7 +1873,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]] @@ -2439,7 +2439,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.11.0", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -2765,7 +2765,7 @@ dependencies = [ "getrandom 0.3.3", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -4436,7 +4436,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 a449027530aaaf5ccfc73b9d9336b9dad582ef59 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 6 Jan 2026 01:58:39 +0000 Subject: [PATCH 089/112] chore(deps): update rust crate proc-macro2 to v1.0.105 --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3697d1dbdbd..c9dc1702aa3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2179,9 +2179,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.104" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0" +checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" dependencies = [ "unicode-ident", ] From e0f70aa193eae3891b027e87cecc1b4f877c396f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 6 Jan 2026 05:01:22 +0000 Subject: [PATCH 090/112] chore(deps): update rust crate quote to v1.0.43 --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c9dc1702aa3..c436039736c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2212,9 +2212,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.42" +version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" dependencies = [ "proc-macro2", ] From aae810e2b5731414b08386b62dc846c2314dab27 Mon Sep 17 00:00:00 2001 From: Phan Trung Thanh Date: Tue, 6 Jan 2026 10:51:26 +0100 Subject: [PATCH 091/112] cp: set status code when encountering circular symbolic links (#9757) * cp: set exit code when encountering circular symbolic links error when copying directory * cp: add test to ensure that cp sets the status code when encountering circular symbolic links during directory copy * cp: check that the output of circular symbolic link test has the correct message * cp: update check for stderr message * cp: update circular symbolic link test to account for directory format in windows * cp: use std::path::MAIN_SEPARATOR_STR for test --- src/uu/cp/src/copydir.rs | 3 +-- tests/by-util/test_cp.rs | 29 +++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/uu/cp/src/copydir.rs b/src/uu/cp/src/copydir.rs index bbd3aba6297..6ac1ae09072 100644 --- a/src/uu/cp/src/copydir.rs +++ b/src/uu/cp/src/copydir.rs @@ -22,7 +22,6 @@ use uucore::fs::{ FileInformation, MissingHandling, ResolveMode, canonicalize, path_ends_with_terminator, }; use uucore::show; -use uucore::show_error; use uucore::translate; use uucore::uio_error; use walkdir::{DirEntry, WalkDir}; @@ -513,7 +512,7 @@ pub(crate) fn copy_directory( } // Print an error message, but continue traversing the directory. - Err(e) => show_error!("{e}"), + Err(e) => show!(CpError::WalkDirErr(e)), } } diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index f0ad9d8ca38..dfd07ec4bd0 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -7445,6 +7445,35 @@ fn test_cp_archive_deref_flag_ordering() { } } +#[test] +fn test_cp_circular_symbolic_links_in_directory() { + let source_dir = "source_dir"; + let target_dir = "target_dir"; + let (at, mut ucmd) = at_and_ucmd!(); + let separator = std::path::MAIN_SEPARATOR_STR; + + at.mkdir(source_dir); + at.symlink_file( + format!("{source_dir}/a").as_str(), + format!("{source_dir}/b").as_str(), + ); + at.symlink_file( + format!("{source_dir}/b").as_str(), + format!("{source_dir}/a").as_str(), + ); + + ucmd.arg(source_dir) + .arg(target_dir) + .arg("-rL") + .fails_with_code(1) + .stderr_contains(format!( + "IO error for operation on {source_dir}{separator}a" + )) + .stderr_contains(format!( + "IO error for operation on {source_dir}{separator}b" + )); +} + /// Test that copying to an existing file maintains its permissions, unix only because .mode() only /// works on Unix #[test] From d77fcfbe93a8b154fb2fb564e43b462d72df22fe Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Tue, 6 Jan 2026 01:30:58 +0900 Subject: [PATCH 092/112] FixPR.yml: Use cargo fetch --target $(rustc --print host-tuple) to save size of fetched crates --- .github/workflows/FixPR.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/FixPR.yml b/.github/workflows/FixPR.yml index e8451c5251b..70f42278c35 100644 --- a/.github/workflows/FixPR.yml +++ b/.github/workflows/FixPR.yml @@ -46,7 +46,7 @@ jobs: # Ensure updated '*/Cargo.lock' # * '*/Cargo.lock' is required to be in a format that `cargo` of MinSRV can interpret (eg, v1-format for MinSRV < v1.38) for dir in "." "fuzz"; do - ( cd "$dir" && (cargo fetch --locked --quiet || cargo +${{ steps.vars.outputs.RUST_MIN_SRV }} update) ) + ( cd "$dir" && (cargo fetch --locked --quiet --target $(rustc --print host-tuple) || cargo +${{ steps.vars.outputs.RUST_MIN_SRV }} update) ) done - name: Info shell: bash @@ -65,7 +65,7 @@ jobs: cargo tree -V ## dependencies echo "## dependency list" - cargo fetch --locked --quiet + cargo fetch --locked --quiet --target $(rustc --print host-tuple) ## * using the 'stable' toolchain is necessary to avoid "unexpected '--filter-platform'" errors RUSTUP_TOOLCHAIN=stable cargo 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 }}') From b5b5cf00a46d19e2a024c4fe10302b39817cbb29 Mon Sep 17 00:00:00 2001 From: oech3 <> Date: Sun, 4 Jan 2026 16:26:57 +0900 Subject: [PATCH 093/112] hashsum, cksum: Move default stdin to clap --- src/uu/cksum/src/cksum.rs | 16 ++++++++-------- src/uu/hashsum/src/hashsum.rs | 13 +++++++------ 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/uu/cksum/src/cksum.rs b/src/uu/cksum/src/cksum.rs index 70c80ae37c7..447e90954f7 100644 --- a/src/uu/cksum/src/cksum.rs +++ b/src/uu/cksum/src/cksum.rs @@ -7,8 +7,7 @@ use clap::builder::ValueParser; use clap::{Arg, ArgAction, Command}; -use std::ffi::{OsStr, OsString}; -use std::iter; +use std::ffi::OsString; use uucore::checksum::compute::{ ChecksumComputeOptions, figure_out_output_format, perform_checksum_computation, }; @@ -121,12 +120,11 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let length = maybe_sanitize_length(algo_cli, input_length)?; - let files = matches.get_many::(options::FILE).map_or_else( - // No files given, read from stdin. - || Box::new(iter::once(OsStr::new("-"))) as Box>, - // At least one file given, read from them. - |files| Box::new(files.map(OsStr::new)) as Box>, - ); + // clap provides the default value -. So we unwrap() safety. + let files = matches + .get_many::(options::FILE) + .unwrap() + .map(|s| s.as_os_str()); if check { // cksum does not support '--check'ing legacy algorithms @@ -200,6 +198,8 @@ pub fn uu_app() -> Command { .hide(true) .action(ArgAction::Append) .value_parser(ValueParser::os_string()) + .default_value("-") + .hide_default_value(true) .value_hint(clap::ValueHint::FilePath), ) .arg( diff --git a/src/uu/hashsum/src/hashsum.rs b/src/uu/hashsum/src/hashsum.rs index eea434d9477..3bc6dcff5b2 100644 --- a/src/uu/hashsum/src/hashsum.rs +++ b/src/uu/hashsum/src/hashsum.rs @@ -163,12 +163,11 @@ pub fn uumain(mut args: impl uucore::Args) -> UResult<()> { let strict = matches.get_flag("strict"); let status = matches.get_flag("status"); - let files = matches.get_many::(options::FILE).map_or_else( - // No files given, read from stdin. - || Box::new(iter::once(OsStr::new("-"))) as Box>, - // At least one file given, read from them. - |files| Box::new(files.map(OsStr::new)) as Box>, - ); + // clap provides the default value -. So we unwrap() safety. + let files = matches + .get_many::(options::FILE) + .unwrap() + .map(|s| s.as_os_str()); if check { // on Windows, allow --binary/--text to be used with --check @@ -340,6 +339,8 @@ pub fn uu_app_common() -> Command { .index(1) .action(ArgAction::Append) .value_name(options::FILE) + .default_value("-") + .hide_default_value(true) .value_hint(clap::ValueHint::FilePath) .value_parser(ValueParser::os_string()), ) From 9710c8021d6564cc408170c04769445405d3c4d7 Mon Sep 17 00:00:00 2001 From: oech3 <> Date: Tue, 6 Jan 2026 00:05:53 +0900 Subject: [PATCH 094/112] bump libc & tmp stop musl-i686 --- .github/workflows/CICD.yml | 4 +++- Cargo.lock | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index 67169f984ed..26620b01291 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -579,7 +579,9 @@ jobs: - { os: ubuntu-latest , target: riscv64gc-unknown-linux-musl , features: feat_os_unix_musl , use-cross: use-cross , skip-tests: true } # - { os: ubuntu-latest , target: x86_64-unknown-linux-gnu , features: feat_selinux , use-cross: use-cross } - { os: ubuntu-latest , target: i686-unknown-linux-gnu , features: "feat_os_unix,test_risky_names", use-cross: use-cross } - - { os: ubuntu-latest , target: i686-unknown-linux-musl , features: feat_os_unix_musl , use-cross: use-cross } + # glibc 2.42 is important more than this platform + # Wait https://github.com/rust-lang/libc/pull/4914 + #- { os: ubuntu-latest , target: i686-unknown-linux-musl , features: feat_os_unix_musl , use-cross: use-cross } - { 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 } diff --git a/Cargo.lock b/Cargo.lock index c436039736c..41f3c0802f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1651,9 +1651,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.178" +version = "0.2.179" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f" [[package]] name = "libloading" From 5fdf8d94adce8b532efcfe18a796de0b6ca8565b Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Mon, 5 Jan 2026 15:08:34 +0100 Subject: [PATCH 095/112] uniq: rename keys_differ to keys_are_equal and adapt the code accordingly --- src/uu/uniq/src/uniq.rs | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/uu/uniq/src/uniq.rs b/src/uu/uniq/src/uniq.rs index 9c95305e4be..ae9b88f2d47 100644 --- a/src/uu/uniq/src/uniq.rs +++ b/src/uu/uniq/src/uniq.rs @@ -95,7 +95,15 @@ impl Uniq { self.build_meta(&next_buf, &mut next_meta); - if self.keys_differ(¤t_buf, ¤t_meta, &next_buf, &next_meta) { + if self.keys_are_equal(¤t_buf, ¤t_meta, &next_buf, &next_meta) { + if self.all_repeated { + self.print_line(writer, ¤t_buf, group_count, first_line_printed)?; + first_line_printed = true; + std::mem::swap(&mut current_buf, &mut next_buf); + std::mem::swap(&mut current_meta, &mut next_meta); + } + group_count += 1; + } else { if (group_count == 1 && !self.repeats_only) || (group_count > 1 && !self.uniques_only) { @@ -105,14 +113,6 @@ impl Uniq { std::mem::swap(&mut current_buf, &mut next_buf); std::mem::swap(&mut current_meta, &mut next_meta); group_count = 1; - } else { - if self.all_repeated { - self.print_line(writer, ¤t_buf, group_count, first_line_printed)?; - first_line_printed = true; - std::mem::swap(&mut current_buf, &mut next_buf); - std::mem::swap(&mut current_meta, &mut next_meta); - } - group_count += 1; } next_buf.clear(); } @@ -136,7 +136,7 @@ impl Uniq { if self.zero_terminated { 0 } else { b'\n' } } - fn keys_differ( + fn keys_are_equal( &self, first_line: &[u8], first_meta: &LineMeta, @@ -146,11 +146,11 @@ impl Uniq { let first_slice = &first_line[first_meta.key_start..first_meta.key_end]; let second_slice = &second_line[second_meta.key_start..second_meta.key_end]; - if !self.ignore_case { - return first_slice != second_slice; + if self.ignore_case { + first_slice.eq_ignore_ascii_case(second_slice) + } else { + first_slice == second_slice } - - !first_slice.eq_ignore_ascii_case(second_slice) } fn key_bounds(&self, line: &[u8]) -> (usize, usize) { From fbe8c55497d2297e83c5745e158a3af76e76d292 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=E1=BA=A3=20th=E1=BA=BF=20gi=E1=BB=9Bi=20l=C3=A0=20Rust?= Date: Mon, 5 Jan 2026 03:21:51 +0000 Subject: [PATCH 096/112] fix: handle write errors gracefully instead of panicking Fixes #9769 Changed error.print().unwrap() to let _ = error.print() to prevent panic when writing to /dev/full. Added regression test in test_cat.rs. --- .../workspace.wordlist.txt | 1 + src/uucore/src/lib/mods/error.rs | 4 ++- tests/by-util/test_cat.rs | 31 +++++++++++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/.vscode/cspell.dictionaries/workspace.wordlist.txt b/.vscode/cspell.dictionaries/workspace.wordlist.txt index f9c8d686bff..28c468d4f9c 100644 --- a/.vscode/cspell.dictionaries/workspace.wordlist.txt +++ b/.vscode/cspell.dictionaries/workspace.wordlist.txt @@ -182,6 +182,7 @@ LINESIZE NAMESIZE RTLD_NEXT RTLD +SIGABRT SIGINT SIGKILL SIGSTOP diff --git a/src/uucore/src/lib/mods/error.rs b/src/uucore/src/lib/mods/error.rs index 0b88e389b65..ef270546cd7 100644 --- a/src/uucore/src/lib/mods/error.rs +++ b/src/uucore/src/lib/mods/error.rs @@ -748,7 +748,9 @@ impl Error for ClapErrorWrapper {} // This is abuse of the Display trait impl Display for ClapErrorWrapper { fn fmt(&self, _f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { - self.error.print().unwrap(); + // Intentionally ignore the result - error.print() writes directly to stderr + // and we always return Ok(()) to satisfy Display's contract + let _ = self.error.print(); Ok(()) } } diff --git a/tests/by-util/test_cat.rs b/tests/by-util/test_cat.rs index 33796f3ae41..2d35a2e2583 100644 --- a/tests/by-util/test_cat.rs +++ b/tests/by-util/test_cat.rs @@ -833,6 +833,37 @@ fn test_child_when_pipe_in() { ts.ucmd().pipe_in("content").run().stdout_is("content"); } +/// Regression test for GitHub issue #9769 +/// https://github.com/uutils/coreutils/issues/9769 +/// +/// Bug: Utilities panic when output is redirected to /dev/full +/// Location: src/uucore/src/lib/mods/error.rs:751 - `.unwrap()` causes panic +/// +/// This test verifies that cat handles write errors to /dev/full gracefully +/// instead of panicking with exit code 134 (SIGABRT). +/// +/// Expected behavior with current BUGGY code: +/// - Test WILL FAIL (cat panics with exit code 134) +/// +/// Expected behavior after fix: +/// - Test SHOULD PASS (cat exits gracefully with error code 1) +// Regression test for issue #9769: graceful error handling when writing to /dev/full +#[test] +#[cfg(target_os = "linux")] +fn test_write_error_handling() { + use std::fs::File; + + let dev_full = + File::create("/dev/full").expect("Failed to open /dev/full - test must run on Linux"); + + new_ucmd!() + .pipe_in("test content that should cause write error to /dev/full") + .set_stdout(dev_full) + .fails() + .code_is(1) + .stderr_contains("No space left on device"); +} + #[test] fn test_cat_eintr_handling() { // Test that cat properly handles EINTR (ErrorKind::Interrupted) during I/O operations From 0ea542d368b1dc3ce9ec02fa74a937b1c92c9e4d Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Tue, 6 Jan 2026 01:01:30 +0900 Subject: [PATCH 097/112] fsext.rs: Replace getmntinfo by libc crate --- src/uucore/src/lib/features/fsext.rs | 35 ++-------------------------- 1 file changed, 2 insertions(+), 33 deletions(-) diff --git a/src/uucore/src/lib/features/fsext.rs b/src/uucore/src/lib/features/fsext.rs index ec88a5e614e..0b6e59acb5d 100644 --- a/src/uucore/src/lib/features/fsext.rs +++ b/src/uucore/src/lib/features/fsext.rs @@ -420,37 +420,6 @@ fn mount_dev_id(mount_dir: &OsStr) -> String { } } -#[cfg(any( - target_os = "freebsd", - target_vendor = "apple", - target_os = "netbsd", - target_os = "openbsd" -))] -use libc::c_int; -#[cfg(any( - target_os = "freebsd", - target_vendor = "apple", - target_os = "netbsd", - target_os = "openbsd" -))] -unsafe extern "C" { - #[cfg(all(target_vendor = "apple", target_arch = "x86_64"))] - #[link_name = "getmntinfo$INODE64"] - fn get_mount_info(mount_buffer_p: *mut *mut StatFs, flags: c_int) -> c_int; - - #[cfg(any( - target_os = "netbsd", - target_os = "openbsd", - all(target_vendor = "apple", target_arch = "aarch64") - ))] - #[link_name = "getmntinfo"] - fn get_mount_info(mount_buffer_p: *mut *mut StatFs, flags: c_int) -> c_int; - - #[cfg(target_os = "freebsd")] - #[link_name = "getmntinfo"] - fn get_mount_info(mount_buffer_p: *mut *mut StatFs, flags: c_int) -> c_int; -} - use crate::error::UResult; #[cfg(any( target_os = "freebsd", @@ -505,9 +474,9 @@ pub fn read_fs_list() -> UResult> { ))] { let mut mount_buffer_ptr: *mut StatFs = ptr::null_mut(); - let len = unsafe { get_mount_info(&raw mut mount_buffer_ptr, 1_i32) }; + let len = unsafe { libc::getmntinfo(&raw mut mount_buffer_ptr, 1_i32) }; if len < 0 { - return Err(USimpleError::new(1, "get_mount_info() failed")); + return Err(USimpleError::new(1, "getmntinfo() failed")); } let mounts = unsafe { slice::from_raw_parts(mount_buffer_ptr, len as usize) }; Ok(mounts From 47805e544f2ab876383579ef9496fe8b3d9f9ff5 Mon Sep 17 00:00:00 2001 From: Aaron Ang <67321817+aaron-ang@users.noreply.github.com> Date: Tue, 6 Jan 2026 15:06:35 -0800 Subject: [PATCH 098/112] fix: use jiff time for touch tests --- Cargo.lock | 1 + Cargo.toml | 1 + tests/by-util/test_touch.rs | 12 ++++++------ 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 41f3c0802f9..c2daa660457 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -546,6 +546,7 @@ dependencies = [ "fluent-syntax", "glob", "hex-literal", + "jiff", "libc", "nix", "num-prime", diff --git a/Cargo.toml b/Cargo.toml index 6a5796a46c6..8c2ce266109 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -539,6 +539,7 @@ chrono.workspace = true ctor.workspace = true filetime.workspace = true glob.workspace = true +jiff.workspace = true libc.workspace = true num-prime.workspace = true pretty_assertions = "1.4.0" diff --git a/tests/by-util/test_touch.rs b/tests/by-util/test_touch.rs index eb2b5c02f6f..25e6b301fec 100644 --- a/tests/by-util/test_touch.rs +++ b/tests/by-util/test_touch.rs @@ -2,11 +2,12 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (formats) cymdhm cymdhms mdhm mdhms ymdhm ymdhms datetime mktime +// spell-checker:ignore (formats) cymdhm cymdhms datetime mdhm mdhms mktime strtime ymdhm ymdhms use filetime::FileTime; #[cfg(not(target_os = "freebsd"))] use filetime::set_symlink_file_times; +use jiff::{fmt::strtime, tz::TimeZone}; use std::fs::remove_file; use std::path::PathBuf; use uutests::at_and_ucmd; @@ -36,11 +37,10 @@ fn set_file_times(at: &AtPath, path: &str, atime: FileTime, mtime: FileTime) { } fn str_to_filetime(format: &str, s: &str) -> FileTime { - let tm = chrono::NaiveDateTime::parse_from_str(s, format).unwrap(); - FileTime::from_unix_time( - tm.and_utc().timestamp(), - tm.and_utc().timestamp_subsec_nanos(), - ) + let tm = strtime::parse(format, s).unwrap(); + let dt = tm.to_datetime().unwrap(); + let ts = dt.to_zoned(TimeZone::UTC).unwrap().timestamp(); + FileTime::from_unix_time(ts.as_second(), ts.subsec_nanosecond() as u32) } #[test] From ef44c0be01b61757319e3b3e6733bfeba2a7c5c2 Mon Sep 17 00:00:00 2001 From: oech3 <> Date: Sun, 4 Jan 2026 12:37:12 +0900 Subject: [PATCH 099/112] hashsum, cksum: Move --ckeck confliction to clap --- src/uu/cksum/src/cksum.rs | 11 +++------ src/uu/hashsum/src/hashsum.rs | 26 ++++++++++----------- src/uucore/src/lib/features/checksum/mod.rs | 2 -- tests/by-util/test_cksum.rs | 4 ++-- 4 files changed, 18 insertions(+), 25 deletions(-) diff --git a/src/uu/cksum/src/cksum.rs b/src/uu/cksum/src/cksum.rs index 447e90954f7..72c7984f049 100644 --- a/src/uu/cksum/src/cksum.rs +++ b/src/uu/cksum/src/cksum.rs @@ -132,14 +132,6 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { return Err(ChecksumError::AlgorithmNotSupportedWithCheck.into()); } - let text_flag = matches.get_flag(options::TEXT); - let binary_flag = matches.get_flag(options::BINARY); - let tag = matches.get_flag(options::TAG); - - if tag || binary_flag || text_flag { - return Err(ChecksumError::BinaryTextConflict.into()); - } - // Execute the checksum validation based on the presence of files or the use of stdin let verbose = ChecksumVerbose::new(status, quiet, warn); @@ -251,6 +243,9 @@ pub fn uu_app() -> Command { .short('c') .long(options::CHECK) .help(translate!("cksum-help-check")) + .conflicts_with(options::TAG) + .conflicts_with(options::BINARY) + .conflicts_with(options::TEXT) .action(ArgAction::SetTrue), ) .arg( diff --git a/src/uu/hashsum/src/hashsum.rs b/src/uu/hashsum/src/hashsum.rs index 3bc6dcff5b2..a13ec468436 100644 --- a/src/uu/hashsum/src/hashsum.rs +++ b/src/uu/hashsum/src/hashsum.rs @@ -170,18 +170,7 @@ pub fn uumain(mut args: impl uucore::Args) -> UResult<()> { .map(|s| s.as_os_str()); if check { - // on Windows, allow --binary/--text to be used with --check - // and keep the behavior of defaulting to binary - #[cfg(not(windows))] - { - let text_flag = matches.get_flag("text"); - let binary_flag = matches.get_flag("binary"); - - if binary_flag || text_flag { - return Err(ChecksumError::BinaryTextConflict.into()); - } - } - + // No reason to allow --check with --binary/--text on Cygwin. It want to be same with Linux and --text was broken for a long time. let verbose = ChecksumVerbose::new(status, quiet, warn); let opts = ChecksumValidateOptions { @@ -231,6 +220,10 @@ mod options { } pub fn uu_app_common() -> Command { + // --text --arg-deps-check should be error by Arg::new(options::CHECK)...conflicts_with(options::TEXT) + // https://github.com/clap-rs/clap/issues/4520 ? + // Let --{warn,strict,quiet,status,ignore-missing} reject --text and remove them later. + // Bad error message, but not a lie... Command::new(uucore::util_name()) .version(uucore::crate_version!()) .help_template(uucore::localized_help_template(uucore::util_name())) @@ -260,7 +253,9 @@ pub fn uu_app_common() -> Command { .long("check") .help(translate!("hashsum-help-check")) .action(ArgAction::SetTrue) - .conflicts_with("tag"), + .conflicts_with(options::BINARY) + .conflicts_with(options::TEXT) + .conflicts_with(options::TAG), ) .arg( Arg::new(options::TAG) @@ -293,6 +288,7 @@ pub fn uu_app_common() -> Command { .help(translate!("hashsum-help-quiet")) .action(ArgAction::SetTrue) .overrides_with_all([options::STATUS, options::WARN]) + .conflicts_with("text") .requires(options::CHECK), ) .arg( @@ -302,6 +298,7 @@ pub fn uu_app_common() -> Command { .help(translate!("hashsum-help-status")) .action(ArgAction::SetTrue) .overrides_with_all([options::QUIET, options::WARN]) + .conflicts_with("text") .requires(options::CHECK), ) .arg( @@ -309,6 +306,7 @@ pub fn uu_app_common() -> Command { .long("strict") .help(translate!("hashsum-help-strict")) .action(ArgAction::SetTrue) + .conflicts_with("text") .requires(options::CHECK), ) .arg( @@ -316,6 +314,7 @@ pub fn uu_app_common() -> Command { .long("ignore-missing") .help(translate!("hashsum-help-ignore-missing")) .action(ArgAction::SetTrue) + .conflicts_with("text") .requires(options::CHECK), ) .arg( @@ -325,6 +324,7 @@ pub fn uu_app_common() -> Command { .help(translate!("hashsum-help-warn")) .action(ArgAction::SetTrue) .overrides_with_all([options::QUIET, options::STATUS]) + .conflicts_with("text") .requires(options::CHECK), ) .arg( diff --git a/src/uucore/src/lib/features/checksum/mod.rs b/src/uucore/src/lib/features/checksum/mod.rs index 7cf7fe129fa..e272cdea68b 100644 --- a/src/uucore/src/lib/features/checksum/mod.rs +++ b/src/uucore/src/lib/features/checksum/mod.rs @@ -390,8 +390,6 @@ pub enum ChecksumError { #[error("--length is only supported with --algorithm blake2b, sha2, or sha3")] LengthOnlyForBlake2bSha2Sha3, - #[error("the --binary and --text options are meaningless when verifying checksums")] - BinaryTextConflict, #[error("--text mode is only supported with --untagged")] TextWithoutUntagged, #[error("--check is not supported with --algorithm={{bsd,sysv,crc,crc32b}}")] diff --git a/tests/by-util/test_cksum.rs b/tests/by-util/test_cksum.rs index d1abe3409ba..40f49fc70f8 100644 --- a/tests/by-util/test_cksum.rs +++ b/tests/by-util/test_cksum.rs @@ -1216,7 +1216,7 @@ fn test_conflicting_options() { .fails_with_code(1) .no_stdout() .stderr_contains( - "cksum: the --binary and --text options are meaningless when verifying checksums", + "cannot be used with", //clap generated error ); scene @@ -1228,7 +1228,7 @@ fn test_conflicting_options() { .fails_with_code(1) .no_stdout() .stderr_contains( - "cksum: the --binary and --text options are meaningless when verifying checksums", + "cannot be used with", //clap generated error ); } From 4594061c349cc4f7926d88981d8565d7a84a5128 Mon Sep 17 00:00:00 2001 From: Christopher Dryden Date: Tue, 6 Jan 2026 20:40:13 +0000 Subject: [PATCH 100/112] ci: add Codecov Test Analytics integration --- .config/nextest.toml | 6 +++++ .github/workflows/CICD.yml | 50 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/.config/nextest.toml b/.config/nextest.toml index 473c461402a..710ff26c59d 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -5,9 +5,15 @@ final-status-level = "skip" failure-output = "immediate-final" fail-fast = false +[profile.ci.junit] +path = "junit.xml" + [profile.coverage] retries = 0 status-level = "all" final-status-level = "skip" failure-output = "immediate-final" fail-fast = false + +[profile.coverage.junit] +path = "junit.xml" diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index 26620b01291..2f8e1035bfe 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -228,6 +228,16 @@ jobs: env: RUSTFLAGS: "-Awarnings" RUST_BACKTRACE: "1" + - name: Upload test results to Codecov + if: ${{ !cancelled() }} + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + report_type: test_results + files: target/nextest/ci/junit.xml + disable_search: true + flags: msrv,${{ matrix.job.os }} + fail_ci_if_error: false deps: name: Dependencies @@ -300,6 +310,16 @@ jobs: run: make nextest PROFILE=ci CARGOFLAGS="--hide-progress-bar" env: RUST_BACKTRACE: "1" + - name: Upload test results to Codecov + if: ${{ !cancelled() }} + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + report_type: test_results + files: target/nextest/ci/junit.xml + disable_search: true + flags: makefile,${{ matrix.job.os }} + fail_ci_if_error: false - name: "`make install PROG_PREFIX=uu- PROFILE=release-fast COMPLETIONS=n MANPAGES=n LOCALES=n`" shell: bash run: | @@ -410,6 +430,16 @@ jobs: run: cargo nextest run --hide-progress-bar --profile ci --features ${{ matrix.job.features }} env: RUST_BACKTRACE: "1" + - name: Upload test results to Codecov + if: ${{ !cancelled() }} + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + report_type: test_results + files: target/nextest/ci/junit.xml + disable_search: true + flags: stable,${{ matrix.job.os }} + fail_ci_if_error: false build_rust_nightly: name: Build/nightly @@ -439,6 +469,16 @@ jobs: run: cargo nextest run --hide-progress-bar --profile ci --features ${{ matrix.job.features }} env: RUST_BACKTRACE: "1" + - name: Upload test results to Codecov + if: ${{ !cancelled() }} + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + report_type: test_results + files: target/nextest/ci/junit.xml + disable_search: true + flags: nightly,${{ matrix.job.os }} + fail_ci_if_error: false compute_size: name: Binary sizes @@ -1158,6 +1198,16 @@ jobs: flags: ${{ steps.vars.outputs.CODECOV_FLAGS }} name: codecov-umbrella fail_ci_if_error: false + - name: Upload test results to Codecov + if: ${{ !cancelled() }} + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + report_type: test_results + files: target/nextest/coverage/junit.xml + disable_search: true + flags: coverage,${{ matrix.job.os }} + fail_ci_if_error: false test_separately: name: Separate Builds From 000800ad4b80c6ac13cfcc7999ea4ab777034fab Mon Sep 17 00:00:00 2001 From: Christopher Dryden Date: Tue, 6 Jan 2026 17:57:30 +0000 Subject: [PATCH 101/112] runcon: fix SELinux test failures by adding chmod to build and fixing -c PATH behavior --- src/uu/runcon/src/runcon.rs | 25 ++++++++++++++++++++----- util/build-gnu.sh | 2 +- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/uu/runcon/src/runcon.rs b/src/uu/runcon/src/runcon.rs index 75fdfbec0f1..60c71d1dca3 100644 --- a/src/uu/runcon/src/runcon.rs +++ b/src/uu/runcon/src/runcon.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 (vars) RFILE +// spell-checker:ignore (vars) RFILE execv execvp #![cfg(target_os = "linux")] use clap::builder::ValueParser; @@ -48,7 +48,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { .map_err(RunconError::new)?; // On successful execution, the following call never returns, // and this process image is replaced. - execute_command(command, &options.arguments) + // PlainContext mode uses PATH search (like execvp). + execute_command(command, &options.arguments, false) } CommandLineMode::CustomContext { compute_transition_context, @@ -72,7 +73,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { .map_err(RunconError::new)?; // On successful execution, the following call never returns, // and this process image is replaced. - execute_command(command, &options.arguments) + // With -c flag, skip PATH search (like execv vs execvp). + execute_command(command, &options.arguments, *compute_transition_context) } None => print_current_context().map_err(|e| RunconError::new(e).into()), } @@ -367,8 +369,21 @@ fn get_custom_context( /// However, until the *never* type is stabilized, one way to indicate to the /// compiler the only valid return type is to say "if this returns, it will /// always return an error". -fn execute_command(command: &OsStr, arguments: &[OsString]) -> UResult<()> { - let err = process::Command::new(command).args(arguments).exec(); +/// +/// When `skip_path_search` is true (used with `-c` flag), the command is executed +/// without PATH lookup, matching GNU's use of execv() vs execvp(). +fn execute_command(command: &OsStr, arguments: &[OsString], skip_path_search: bool) -> UResult<()> { + // When skip_path_search is true and command has no path separator, + // prepend "./" to prevent PATH lookup (like execv vs execvp). + let command_path = if skip_path_search && !command.as_bytes().contains(&b'/') { + let mut path = OsString::from("./"); + path.push(command); + path + } else { + command.to_os_string() + }; + + let err = process::Command::new(&command_path).args(arguments).exec(); let exit_status = if err.kind() == io::ErrorKind::NotFound { error_exit_status::NOT_FOUND diff --git a/util/build-gnu.sh b/util/build-gnu.sh index 421b43d5e7e..6075c863727 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -93,7 +93,7 @@ export CARGOFLAGS # tell to make ln -vf "${UU_BUILD_DIR}/install" "${UU_BUILD_DIR}/ginstall" # The GNU tests use renamed install to ginstall if [ "${SELINUX_ENABLED}" = 1 ];then # Build few utils for SELinux for faster build. MULTICALL=y fails... - "${MAKE}" UTILS="cat chcon cp cut echo env groups id ln ls mkdir mkfifo mknod mktemp mv printf rm rmdir runcon stat test touch tr true uname wc whoami" + "${MAKE}" UTILS="cat chcon chmod cp cut echo env groups id ln ls mkdir mkfifo mknod mktemp mv printf rm rmdir runcon stat test touch tr true uname wc whoami" else # Use MULTICALL=y for faster build "${MAKE}" MULTICALL=y SKIP_UTILS="install more seq" From 487dde473da7893192f9c4663cca1cc272acd730 Mon Sep 17 00:00:00 2001 From: Chris Dryden Date: Wed, 7 Jan 2026 01:29:57 -0500 Subject: [PATCH 102/112] cp: use FileInformation without dereference for symlink destination check to match GNU behaviour for test/nfs-removal-race (#10086) * cp: use lstat for destination check to support LD_PRELOAD tests * fs: fix Windows is_symlink type mismatch and add explanatory comment * cp: use stat for dest existence check to support LD_PRELOAD --- src/uu/cp/src/cp.rs | 4 ++-- util/fetch-gnu.sh | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index 3048f38b711..cd84caa36af 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -1390,8 +1390,8 @@ pub fn copy(sources: &[PathBuf], target: &Path, options: &Options) -> CopyResult let dest = construct_dest_path(source, target, target_type, options) .unwrap_or_else(|_| target.to_path_buf()); - if fs::metadata(&dest).is_ok() - && !fs::symlink_metadata(&dest)?.file_type().is_symlink() + if FileInformation::from_path(&dest, true).is_ok() + && !fs::symlink_metadata(&dest).is_ok_and(|m| m.file_type().is_symlink()) // if both `source` and `dest` are symlinks, it should be considered as an overwrite. || fs::metadata(source).is_ok() && fs::symlink_metadata(source)?.file_type().is_symlink() diff --git a/util/fetch-gnu.sh b/util/fetch-gnu.sh index 92e88ed75c6..34bb3fb9c5e 100755 --- a/util/fetch-gnu.sh +++ b/util/fetch-gnu.sh @@ -7,6 +7,7 @@ curl -L "${repo}/releases/download/v${ver}/coreutils-${ver}.tar.xz" | tar --stri curl -L ${repo}/raw/refs/heads/master/tests/mv/hardlink-case.sh > tests/mv/hardlink-case.sh curl -L ${repo}/raw/refs/heads/master/tests/mkdir/writable-under-readonly.sh > tests/mkdir/writable-under-readonly.sh curl -L ${repo}/raw/refs/heads/master/tests/cp/cp-mv-enotsup-xattr.sh > tests/cp/cp-mv-enotsup-xattr.sh #spell-checker:disable-line +curl -L ${repo}/raw/refs/heads/master/tests/cp/nfs-removal-race.sh > tests/cp/nfs-removal-race.sh curl -L ${repo}/raw/refs/heads/master/tests/csplit/csplit-io-err.sh > tests/csplit/csplit-io-err.sh # Avoid incorrect PASS curl -L ${repo}/raw/refs/heads/master/tests/runcon/runcon-compute.sh > tests/runcon/runcon-compute.sh From 35a3287939e50fce24a059c342a31798c3d0dad0 Mon Sep 17 00:00:00 2001 From: Chris Dryden Date: Wed, 7 Jan 2026 02:42:19 -0500 Subject: [PATCH 103/112] Add bad-speed.sh test script to fetch-gnu.sh (#10077) --- util/fetch-gnu.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/util/fetch-gnu.sh b/util/fetch-gnu.sh index 34bb3fb9c5e..caecdec7da3 100755 --- a/util/fetch-gnu.sh +++ b/util/fetch-gnu.sh @@ -9,5 +9,6 @@ curl -L ${repo}/raw/refs/heads/master/tests/mkdir/writable-under-readonly.sh > t curl -L ${repo}/raw/refs/heads/master/tests/cp/cp-mv-enotsup-xattr.sh > tests/cp/cp-mv-enotsup-xattr.sh #spell-checker:disable-line curl -L ${repo}/raw/refs/heads/master/tests/cp/nfs-removal-race.sh > tests/cp/nfs-removal-race.sh curl -L ${repo}/raw/refs/heads/master/tests/csplit/csplit-io-err.sh > tests/csplit/csplit-io-err.sh +curl -L ${repo}/raw/refs/heads/master/tests/stty/bad-speed.sh > tests/stty/bad-speed.sh # Avoid incorrect PASS curl -L ${repo}/raw/refs/heads/master/tests/runcon/runcon-compute.sh > tests/runcon/runcon-compute.sh From 95d85e59314d0b6190e6ea704dfe69df99d058e6 Mon Sep 17 00:00:00 2001 From: xtqqczze <45661989+xtqqczze@users.noreply.github.com> Date: Sun, 4 Jan 2026 23:10:29 +0000 Subject: [PATCH 104/112] clippy: fix used_underscore_binding lint https://rust-lang.github.io/rust-clippy/master/index.html#used_underscore_binding --- Cargo.toml | 1 - src/uu/df/src/filesystem.rs | 6 +++--- src/uu/env/src/env.rs | 28 +++++++++++++--------------- src/uu/mv/src/mv.rs | 8 +------- src/uu/split/src/platform/unix.rs | 14 +++++++------- src/uu/stat/src/stat.rs | 5 +++-- src/uu/stty/src/stty.rs | 8 ++++++-- src/uu/tail/src/paths.rs | 4 ++-- src/uu/who/src/platform/unix.rs | 11 ++++------- src/uucore/src/lib/features/fs.rs | 7 +++++-- src/uucore/src/lib/features/sum.rs | 8 ++++---- 11 files changed, 48 insertions(+), 52 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8c2ce266109..d28e8628dcb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -662,7 +662,6 @@ ignored_unit_patterns = "allow" # 21 similar_names = "allow" # 20 large_stack_arrays = "allow" # 20 wildcard_imports = "allow" # 18 -used_underscore_binding = "allow" # 18 needless_pass_by_value = "allow" # 16 float_cmp = "allow" # 12 items_after_statements = "allow" # 11 diff --git a/src/uu/df/src/filesystem.rs b/src/uu/df/src/filesystem.rs index 041f739596b..7d9e7cf5ef0 100644 --- a/src/uu/df/src/filesystem.rs +++ b/src/uu/df/src/filesystem.rs @@ -121,7 +121,7 @@ where impl Filesystem { // TODO: resolve uuid in `mount_info.dev_name` if exists pub(crate) fn new(mount_info: MountInfo, file: Option) -> Option { - let _stat_path = if mount_info.mount_dir.is_empty() { + let stat_path = if mount_info.mount_dir.is_empty() { #[cfg(unix)] { mount_info.dev_name.clone().into() @@ -135,9 +135,9 @@ impl Filesystem { mount_info.mount_dir.clone() }; #[cfg(unix)] - let usage = FsUsage::new(statfs(&_stat_path).ok()?); + let usage = FsUsage::new(statfs(&stat_path).ok()?); #[cfg(windows)] - let usage = FsUsage::new(Path::new(&_stat_path)).ok()?; + let usage = FsUsage::new(Path::new(&stat_path)).ok()?; Some(Self { file, mount_info, diff --git a/src/uu/env/src/env.rs b/src/uu/env/src/env.rs index e71581f8617..d715d3e9ec1 100644 --- a/src/uu/env/src/env.rs +++ b/src/uu/env/src/env.rs @@ -749,27 +749,25 @@ impl EnvAppData { do_debug_printing: bool, ) -> Result<(), Box> { let prog = Cow::from(opts.program[0]); - #[cfg(unix)] - let mut arg0 = prog.clone(); - #[cfg(not(unix))] - let arg0 = prog.clone(); - let args = &opts.program[1..]; - if let Some(_argv0) = opts.argv0 { - #[cfg(unix)] - { - arg0 = Cow::Borrowed(_argv0); + let arg0 = match opts.argv0 { + None => prog.clone(), + Some(argv0) if cfg!(unix) => { + let arg0 = Cow::Borrowed(argv0); if do_debug_printing { eprintln!("argv0: {}", arg0.quote()); } + arg0 + } + Some(_) => { + return Err(USimpleError::new( + 2, + translate!("env-error-argv0-not-supported"), + )); } + }; - #[cfg(not(unix))] - return Err(USimpleError::new( - 2, - translate!("env-error-argv0-not-supported"), - )); - } + let args = &opts.program[1..]; if do_debug_printing { eprintln!("executing: {}", prog.maybe_quote()); diff --git a/src/uu/mv/src/mv.rs b/src/uu/mv/src/mv.rs index aa34a6294ae..860683e7a6e 100644 --- a/src/uu/mv/src/mv.rs +++ b/src/uu/mv/src/mv.rs @@ -1027,7 +1027,7 @@ fn copy_dir_contents( } #[cfg(not(unix))] { - copy_dir_contents_recursive(from, to, None, None, verbose, progress_bar, display_manager)?; + copy_dir_contents_recursive(from, to, verbose, progress_bar, display_manager)?; } Ok(()) @@ -1038,8 +1038,6 @@ fn copy_dir_contents_recursive( to_dir: &Path, #[cfg(unix)] hardlink_tracker: &mut HardlinkTracker, #[cfg(unix)] hardlink_scanner: &HardlinkGroupScanner, - #[cfg(not(unix))] _hardlink_tracker: Option<()>, - #[cfg(not(unix))] _hardlink_scanner: Option<()>, verbose: bool, progress_bar: Option<&ProgressBar>, display_manager: Option<&MultiProgress>, @@ -1078,10 +1076,6 @@ fn copy_dir_contents_recursive( hardlink_tracker, #[cfg(unix)] hardlink_scanner, - #[cfg(not(unix))] - _hardlink_tracker, - #[cfg(not(unix))] - _hardlink_scanner, verbose, progress_bar, display_manager, diff --git a/src/uu/split/src/platform/unix.rs b/src/uu/split/src/platform/unix.rs index d530ee25966..656bd0109be 100644 --- a/src/uu/split/src/platform/unix.rs +++ b/src/uu/split/src/platform/unix.rs @@ -43,9 +43,9 @@ impl Write for FilterWriter { /// Have an environment variable set at a value during this lifetime struct WithEnvVarSet { /// Env var key - _previous_var_key: String, + previous_var_key: String, /// Previous value set to this key - _previous_var_value: std::result::Result, + previous_var_value: std::result::Result, } impl WithEnvVarSet { /// Save previous value assigned to key, set key=value @@ -55,8 +55,8 @@ impl WithEnvVarSet { env::set_var(key, value); } Self { - _previous_var_key: String::from(key), - _previous_var_value: previous_env_value, + previous_var_key: String::from(key), + previous_var_value: previous_env_value, } } } @@ -64,13 +64,13 @@ 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 Ok(ref prev_value) = self.previous_var_value { unsafe { - env::set_var(&self._previous_var_key, prev_value); + env::set_var(&self.previous_var_key, prev_value); } } else { unsafe { - env::remove_var(&self._previous_var_key); + env::remove_var(&self.previous_var_key); } } } diff --git a/src/uu/stat/src/stat.rs b/src/uu/stat/src/stat.rs index bae89461cb8..48430a26157 100644 --- a/src/uu/stat/src/stat.rs +++ b/src/uu/stat/src/stat.rs @@ -1017,7 +1017,8 @@ impl Stater { file: &OsString, file_type: &FileType, from_user: bool, - _follow_symbolic_links: bool, + #[cfg(feature = "selinux")] follow_symbolic_links: bool, + #[cfg(not(feature = "selinux"))] _: bool, ) -> Result<(), i32> { match *t { Token::Byte(byte) => write_raw_byte(byte), @@ -1048,7 +1049,7 @@ impl Stater { if uucore::selinux::is_selinux_enabled() { match uucore::selinux::get_selinux_security_context( Path::new(file), - _follow_symbolic_links, + follow_symbolic_links, ) { Ok(ctx) => OutputType::Str(ctx), Err(_) => OutputType::Str(translate!( diff --git a/src/uu/stty/src/stty.rs b/src/uu/stty/src/stty.rs index 9153c1528fe..8808857b630 100644 --- a/src/uu/stty/src/stty.rs +++ b/src/uu/stty/src/stty.rs @@ -1033,11 +1033,15 @@ fn apply_special_setting( match setting { SpecialSetting::Rows(n) => size.rows = *n, SpecialSetting::Cols(n) => size.columns = *n, - SpecialSetting::Line(_n) => { + #[cfg_attr( + not(any(target_os = "linux", target_os = "android")), + expect(unused_variables) + )] + SpecialSetting::Line(n) => { // nix only defines Termios's `line_discipline` field on these platforms #[cfg(any(target_os = "linux", target_os = "android"))] { - _termios.line_discipline = *_n; + _termios.line_discipline = *n; } } } diff --git a/src/uu/tail/src/paths.rs b/src/uu/tail/src/paths.rs index 340a0b29dec..3f37091d8cd 100644 --- a/src/uu/tail/src/paths.rs +++ b/src/uu/tail/src/paths.rs @@ -179,10 +179,10 @@ impl MetadataExtTail for Metadata { Ok(other.len() < self.len() && other.modified()? != self.modified()?) } - fn file_id_eq(&self, _other: &Metadata) -> bool { + fn file_id_eq(&self, #[cfg(unix)] other: &Metadata, #[cfg(not(unix))] _: &Metadata) -> bool { #[cfg(unix)] { - self.ino().eq(&_other.ino()) + self.ino().eq(&other.ino()) } #[cfg(windows)] { diff --git a/src/uu/who/src/platform/unix.rs b/src/uu/who/src/platform/unix.rs index 8e72a83ba39..5cd27f26b92 100644 --- a/src/uu/who/src/platform/unix.rs +++ b/src/uu/who/src/platform/unix.rs @@ -195,13 +195,10 @@ fn current_tty() -> String { impl Who { #[allow(clippy::cognitive_complexity)] fn exec(&mut self) -> UResult<()> { - let run_level_chk = |_record: i16| { - #[cfg(not(target_os = "linux"))] - return false; - - #[cfg(target_os = "linux")] - return _record == utmpx::RUN_LVL; - }; + #[cfg(target_os = "linux")] + let run_level_chk = |record: i16| record == utmpx::RUN_LVL; + #[cfg(not(target_os = "linux"))] + let run_level_chk = |_| false; let f = if self.args.len() == 1 { self.args[0].as_ref() diff --git a/src/uucore/src/lib/features/fs.rs b/src/uucore/src/lib/features/fs.rs index bebfd1821cf..a783d04eace 100644 --- a/src/uucore/src/lib/features/fs.rs +++ b/src/uucore/src/lib/features/fs.rs @@ -761,10 +761,13 @@ pub mod sane_blksize { /// /// If the metadata contain invalid values a meaningful adaption /// of that value is done. - pub fn sane_blksize_from_metadata(_metadata: &std::fs::Metadata) -> u64 { + pub fn sane_blksize_from_metadata( + #[cfg(unix)] metadata: &std::fs::Metadata, + #[cfg(not(unix))] _: &std::fs::Metadata, + ) -> u64 { #[cfg(not(target_os = "windows"))] { - sane_blksize(_metadata.blksize()) + sane_blksize(metadata.blksize()) } #[cfg(target_os = "windows")] diff --git a/src/uucore/src/lib/features/sum.rs b/src/uucore/src/lib/features/sum.rs index 66fb752abaf..6d190edbebd 100644 --- a/src/uucore/src/lib/features/sum.rs +++ b/src/uucore/src/lib/features/sum.rs @@ -284,8 +284,8 @@ impl Digest for Bsd { } fn result(&mut self) -> DigestOutput { - let mut _out = [0; 2]; - self.hash_finalize(&mut _out); + let mut out = [0; 2]; + self.hash_finalize(&mut out); DigestOutput::U16(self.state) } @@ -319,8 +319,8 @@ impl Digest for SysV { } fn result(&mut self) -> DigestOutput { - let mut _out = [0; 2]; - self.hash_finalize(&mut _out); + let mut out = [0; 2]; + self.hash_finalize(&mut out); DigestOutput::U16((self.state & (u16::MAX as u32)) as u16) } From e96c4cffd0070830ccd40303805ee4f07ac4e0cb Mon Sep 17 00:00:00 2001 From: cerdelen <95369756+cerdelen@users.noreply.github.com> Date: Wed, 7 Jan 2026 10:49:54 +0100 Subject: [PATCH 105/112] Chmod preserve root (#10033) * chmod: Fix --preserve-root not being bypassed by path that resolves to root * chmod: Regression tests for --preserve-root not being bypassed by path that resolves to root --- src/uu/chmod/src/chmod.rs | 6 +++++- tests/by-util/test_chmod.rs | 11 +++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/uu/chmod/src/chmod.rs b/src/uu/chmod/src/chmod.rs index b77de93f2b7..43760b45017 100644 --- a/src/uu/chmod/src/chmod.rs +++ b/src/uu/chmod/src/chmod.rs @@ -407,7 +407,7 @@ impl Chmoder { // should not change the permissions in this case continue; } - if self.recursive && self.preserve_root && file == Path::new("/") { + if self.recursive && self.preserve_root && Self::is_root(file) { return Err(ChmodError::PreserveRoot("/".into()).into()); } if self.recursive { @@ -419,6 +419,10 @@ impl Chmoder { r } + fn is_root(file: impl AsRef) -> bool { + matches!(fs::canonicalize(&file), Ok(p) if p == Path::new("/")) + } + #[cfg(not(target_os = "linux"))] fn walk_dir_with_context(&self, file_path: &Path, is_command_line_arg: bool) -> UResult<()> { let mut r = self.chmod_file(file_path); diff --git a/tests/by-util/test_chmod.rs b/tests/by-util/test_chmod.rs index 6d242020ce3..a17fc4a2ca3 100644 --- a/tests/by-util/test_chmod.rs +++ b/tests/by-util/test_chmod.rs @@ -508,6 +508,17 @@ fn test_chmod_preserve_root() { .stderr_contains("chmod: it is dangerous to operate recursively on '/'"); } +#[test] +fn test_chmod_preserve_root_with_paths_that_resolve_to_root() { + new_ucmd!() + .arg("-R") + .arg("--preserve-root") + .arg("755") + .arg("/../") + .fails_with_code(1) + .stderr_contains("chmod: it is dangerous to operate recursively on '/'"); +} + #[test] fn test_chmod_symlink_non_existing_file() { let scene = TestScenario::new(util_name!()); From bd8ddae7dd34950cc15141c1554b029335a290fa Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 6 Jan 2026 17:11:30 +0000 Subject: [PATCH 106/112] chore(deps): update rust crate libc to v0.2.179 --- fuzz/Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index 2b519a989f3..b891522ccca 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -894,9 +894,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.178" +version = "0.2.179" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f" [[package]] name = "libfuzzer-sys" From 0ca1f021f94891dd8e3ea4649e3d2a34c50a4ffe Mon Sep 17 00:00:00 2001 From: mattsu <35655889+mattsu2020@users.noreply.github.com> Date: Wed, 7 Jan 2026 20:16:13 +0900 Subject: [PATCH 107/112] bench(sort): add general numeric benchmark (#10101) --------- Co-authored-by: Sylvestre Ledru --- src/uu/sort/benches/sort_bench.rs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/uu/sort/benches/sort_bench.rs b/src/uu/sort/benches/sort_bench.rs index a4da0ce6c33..4bd72cf629d 100644 --- a/src/uu/sort/benches/sort_bench.rs +++ b/src/uu/sort/benches/sort_bench.rs @@ -128,6 +128,32 @@ fn sort_numeric(bencher: Bencher, num_lines: usize) { }); } +/// Benchmark general numeric sorting (-g) with decimal and exponent notation +#[divan::bench(args = [200_000])] +fn sort_general_numeric(bencher: Bencher, num_lines: usize) { + let mut data = Vec::new(); + + // Generate numeric data with decimal points and exponents + for i in 0..num_lines { + let int_part = (i * 13) % 100_000; + let frac_part = (i * 7) % 1000; + let exp = (i % 5) as i32 - 2; // -2..=2 + let sign = if i % 2 == 0 { "" } else { "-" }; + data.extend_from_slice(format!("{sign}{int_part}.{frac_part:03}e{exp:+}\n").as_bytes()); + } + + let file_path = setup_test_file(&data); + let output_file = NamedTempFile::new().unwrap(); + let output_path = output_file.path().to_str().unwrap(); + + bencher.bench(|| { + black_box(run_util_function( + uumain, + &["-g", "-o", output_path, file_path.to_str().unwrap()], + )); + }); +} + /// Benchmark reverse sorting with locale-aware data #[divan::bench(args = [500_000])] fn sort_reverse_locale(bencher: Bencher, num_lines: usize) { From 4f74454f05c904ec7ec5d6706e1f77acdeb6f862 Mon Sep 17 00:00:00 2001 From: Stephen Marz Date: Wed, 7 Jan 2026 08:12:09 -0500 Subject: [PATCH 108/112] test_install: add two checks that should give Permission denied. One for failing to remove an existing file and one for trying to stat a file in /root. --- tests/by-util/test_install.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/by-util/test_install.rs b/tests/by-util/test_install.rs index b6a998a02aa..9df72dc4f1e 100644 --- a/tests/by-util/test_install.rs +++ b/tests/by-util/test_install.rs @@ -119,6 +119,24 @@ fn test_install_ancestors_mode_directories() { assert_eq!(0o40_200_u32, at.metadata(target_dir).permissions().mode()); } +#[test] +fn test_install_remove_impermissible_dst_file() { + let src_file = "/dev/null"; + let dst_file = "/dev/full"; + new_ucmd!().args(&[src_file, dst_file]).fails().stderr_only(format!( + "install: failed to remove existing file '{dst_file}': Permission denied\n" + )); +} + +#[test] +fn test_install_remove_inaccessible_dst_file() { + let src_file = "/dev/null"; + let dst_file = "/root/file"; + new_ucmd!().args(&[src_file, dst_file]).fails().stderr_only(format!( + "install: cannot stat '{dst_file}': Permission denied\n" + )); +} + #[test] fn test_install_ancestors_mode_directories_with_file() { let (at, mut ucmd) = at_and_ucmd!(); From b0847c474fd9d4a72f683db9f02d2f862b50d28f Mon Sep 17 00:00:00 2001 From: Stephen Marz Date: Wed, 7 Jan 2026 08:12:59 -0500 Subject: [PATCH 109/112] install: update error messages and remove the additional Rust errorisms (e.g., (os error 13)) by using UIoError to print the error message. --- src/uu/install/src/install.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/uu/install/src/install.rs b/src/uu/install/src/install.rs index 2cea23b1e05..24270bf0201 100644 --- a/src/uu/install/src/install.rs +++ b/src/uu/install/src/install.rs @@ -34,6 +34,7 @@ use uucore::selinux::{ }; use uucore::translate; use uucore::{format_usage, show, show_error, show_if_err}; +use uucore::error::UIoError; #[cfg(unix)] use std::os::unix::fs::{FileTypeExt, MetadataExt}; @@ -854,7 +855,8 @@ fn copy_file(from: &Path, to: &Path) -> UResult<()> { // see if the file exists, and it can't even be checked, this means we // don't have permission to access the file, so we should return an error. if let Err(to_stat) = to.try_exists() { - return Err(InstallError::CannotStat(to.to_path_buf(), to_stat.to_string()).into()); + let err = UIoError::from(to_stat); + return Err(InstallError::CannotStat(to.to_path_buf(), err.to_string()).into()); } if to.is_dir() && !from.is_dir() { @@ -871,7 +873,8 @@ fn copy_file(from: &Path, to: &Path) -> UResult<()> { // If we get here, then remove_file failed for some // reason other than the file not existing. This means // this should be a fatal error, not a warning. - return Err(InstallError::FailedToRemove(to.to_path_buf(), e.to_string()).into()); + let err = UIoError::from(e); + return Err(InstallError::FailedToRemove(to.to_path_buf(), err.to_string()).into()); } } From d3e90d9be50eefe578d8c72d24c0eaa295aca91b Mon Sep 17 00:00:00 2001 From: Stephen Marz Date: Wed, 7 Jan 2026 08:22:42 -0500 Subject: [PATCH 110/112] install and test_install: run cargo 'fmt' --- src/uu/install/src/install.rs | 2 +- tests/by-util/test_install.rs | 18 ++++++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/uu/install/src/install.rs b/src/uu/install/src/install.rs index 24270bf0201..4dc03ae92a6 100644 --- a/src/uu/install/src/install.rs +++ b/src/uu/install/src/install.rs @@ -23,6 +23,7 @@ use uucore::backup_control::{self, BackupMode}; use uucore::buf_copy::copy_stream; use uucore::display::Quotable; use uucore::entries::{grp2gid, usr2uid}; +use uucore::error::UIoError; use uucore::error::{FromIo, UError, UResult, UUsageError}; use uucore::fs::dir_strip_dot_for_creation; use uucore::perms::{Verbosity, VerbosityLevel, wrap_chown}; @@ -34,7 +35,6 @@ use uucore::selinux::{ }; use uucore::translate; use uucore::{format_usage, show, show_error, show_if_err}; -use uucore::error::UIoError; #[cfg(unix)] use std::os::unix::fs::{FileTypeExt, MetadataExt}; diff --git a/tests/by-util/test_install.rs b/tests/by-util/test_install.rs index 9df72dc4f1e..e2c039222a3 100644 --- a/tests/by-util/test_install.rs +++ b/tests/by-util/test_install.rs @@ -123,18 +123,24 @@ fn test_install_ancestors_mode_directories() { fn test_install_remove_impermissible_dst_file() { let src_file = "/dev/null"; let dst_file = "/dev/full"; - new_ucmd!().args(&[src_file, dst_file]).fails().stderr_only(format!( - "install: failed to remove existing file '{dst_file}': Permission denied\n" - )); + new_ucmd!() + .args(&[src_file, dst_file]) + .fails() + .stderr_only(format!( + "install: failed to remove existing file '{dst_file}': Permission denied\n" + )); } #[test] fn test_install_remove_inaccessible_dst_file() { let src_file = "/dev/null"; let dst_file = "/root/file"; - new_ucmd!().args(&[src_file, dst_file]).fails().stderr_only(format!( - "install: cannot stat '{dst_file}': Permission denied\n" - )); + new_ucmd!() + .args(&[src_file, dst_file]) + .fails() + .stderr_only(format!( + "install: cannot stat '{dst_file}': Permission denied\n" + )); } #[test] From 4255a20389eca5a39edaf4b19394e433d52534d9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 7 Jan 2026 12:42:22 +0000 Subject: [PATCH 111/112] chore(deps): update rust crate divan to v4.2.1 --- Cargo.lock | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c2daa660457..c38215bfff7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -392,9 +392,9 @@ dependencies = [ [[package]] name = "codspeed" -version = "4.2.0" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb56923193c76a0e5b6b17b2c2bb1e151ef8a5e06b557e1cbe38c6db467763f9" +checksum = "5f0d98d97fd75ca4489a1a0997820a6521531085e7c8a98941bd0e1264d567dd" dependencies = [ "anyhow", "cc", @@ -410,9 +410,9 @@ dependencies = [ [[package]] name = "codspeed-divan-compat" -version = "4.2.0" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7558ff5740fbc26a5fc55c4934cfed94dfccee76abc17b57ecf5d0bee3592b5e" +checksum = "4179ec5518e79efcd02ed50aa483ff807902e43c85146e87fff58b9cffc06078" dependencies = [ "clap", "codspeed", @@ -423,9 +423,9 @@ dependencies = [ [[package]] name = "codspeed-divan-compat-macros" -version = "4.2.0" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de343ca0a4fbaabbd3422941fdee24407d00e2fa686a96021c21a78ab2bb895" +checksum = "15eaee97aa5bceb32cc683fe25cd6373b7fc48baee5c12471996b58b6ddf0d7c" dependencies = [ "divan-macros", "itertools 0.14.0", @@ -437,9 +437,9 @@ dependencies = [ [[package]] name = "codspeed-divan-compat-walltime" -version = "4.2.0" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d9de586cc7e9752fc232f08e0733c2016122e16065c4adf0c8a8d9e370749ee" +checksum = "c38671153aa73be075d6019cab5ab1e6b31d36644067c1ac4cef73bf9723ce33" dependencies = [ "cfg-if", "clap", @@ -1576,7 +1576,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde_core", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1874,7 +1874,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]] @@ -2440,7 +2440,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.11.0", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2766,7 +2766,7 @@ dependencies = [ "getrandom 0.3.3", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4437,7 +4437,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]] From 98121210843435167309e068fdf754c82626aa8d Mon Sep 17 00:00:00 2001 From: mattsu Date: Wed, 7 Jan 2026 20:51:21 +0900 Subject: [PATCH 112/112] refactor(uptime): use FluentArgs for loadavg formatting in get_formatted_loadavg Refactored the `get_formatted_loadavg` function to explicitly build a `FluentArgs` struct with load average values formatted to two decimal places, then pass it to `crate::locale::get_message_with_args` instead of using the `translate!` macro inline. This improves argument handling for fluent localization, enhancing code structure and maintainability without altering functionality. --- src/uucore/src/lib/features/uptime.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/uucore/src/lib/features/uptime.rs b/src/uucore/src/lib/features/uptime.rs index 9dbf878d7e8..91352f1de0d 100644 --- a/src/uucore/src/lib/features/uptime.rs +++ b/src/uucore/src/lib/features/uptime.rs @@ -421,11 +421,13 @@ pub fn get_loadavg() -> UResult<(f64, f64, f64)> { #[inline] pub fn get_formatted_loadavg() -> UResult { let loadavg = get_loadavg()?; - Ok(translate!( + let mut args = fluent::FluentArgs::new(); + args.set("avg1", format!("{:.2}", loadavg.0)); + args.set("avg5", format!("{:.2}", loadavg.1)); + args.set("avg15", format!("{:.2}", loadavg.2)); + Ok(crate::locale::get_message_with_args( "uptime-lib-format-loadavg", - "avg1" => format!("{:.2}", loadavg.0), - "avg5" => format!("{:.2}", loadavg.1), - "avg15" => format!("{:.2}", loadavg.2), + args, )) }