From 08064bc253f8a2ed0b37b2d78a7c4a095053e028 Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Mon, 27 Oct 2025 16:20:09 +0100 Subject: [PATCH 001/182] ci: add locales for GNU tests Iran, Ethiopia, and Thailand --- .github/workflows/GnuTests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/GnuTests.yml b/.github/workflows/GnuTests.yml index fd71e2da1..d348b0b4b 100644 --- a/.github/workflows/GnuTests.yml +++ b/.github/workflows/GnuTests.yml @@ -89,6 +89,9 @@ jobs: sudo locale-gen --keep-existing en_US sudo locale-gen --keep-existing en_US.UTF-8 sudo locale-gen --keep-existing ru_RU.KOI8-R + sudo locale-gen --keep-existing fa_IR.UTF-8 # Iran + sudo locale-gen --keep-existing am_ET.UTF-8 # Ethiopia + sudo locale-gen --keep-existing th_TH.UTF-8 # Thailand sudo update-locale echo "After:" From 550702c67bf0419f8cdca567749b32034975a305 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sat, 8 Nov 2025 20:00:15 +0100 Subject: [PATCH 002/182] github action: add openbsd in the ci --- .github/workflows/openbsd.yml | 195 ++++++++++++++++++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 .github/workflows/openbsd.yml diff --git a/.github/workflows/openbsd.yml b/.github/workflows/openbsd.yml new file mode 100644 index 000000000..9dad6ebc8 --- /dev/null +++ b/.github/workflows/openbsd.yml @@ -0,0 +1,195 @@ +name: OpenBSD + +# spell-checker:ignore sshfs usesh vmactions taiki Swatinem esac fdescfs fdesc sccache nextest copyback logind bindgen libclang + +env: + # * style job configuration + STYLE_FAIL_ON_FAULT: true ## (bool) fail the build if a style job contains a fault (error or warning); may be overridden on a per-job basis + +on: + pull_request: + push: + branches: + - '*' + +permissions: + contents: read # to fetch code (actions/checkout) + +# End the current execution if there is a new changeset in the PR. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + style: + name: Style and Lint + runs-on: ubuntu-latest + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + job: + - { features: unix } + steps: + - uses: actions/checkout@v5 + with: + persist-credentials: false + - name: Prepare, build and test + uses: vmactions/openbsd-vm@v1 + with: + usesh: true + sync: rsync + copyback: false + mem: 4096 + # We need jq and GNU coreutils to run show-utils.sh and bash to use inline shell string replacement + # Use sudo-- to get the default sudo package without ambiguity + # Install rust and cargo from OpenBSD packages + prepare: pkg_add curl sudo-- jq coreutils bash rust rust-clippy rust-rustfmt llvm-- + run: | + ## Prepare, build, and test + # implementation modelled after ref: + # * NOTE: All steps need to be run in this block, otherwise, we are operating back on the mac host + set -e + # + TEST_USER=tester + REPO_NAME=${GITHUB_WORKSPACE##*/} + WORKSPACE_PARENT="/home/runner/work/${REPO_NAME}" + WORKSPACE="${WORKSPACE_PARENT}/${REPO_NAME}" + # + useradd -m -G wheel ${TEST_USER} + chown -R ${TEST_USER}:wheel /root/ "${WORKSPACE_PARENT}"/ + whoami + # + # Further work needs to be done in a sudo as we are changing users + sudo -i -u ${TEST_USER} bash << EOF + set -e + whoami + # Rust is installed from packages, no need for rustup + # Set up PATH for cargo + export PATH="/usr/local/bin:$PATH" + ## VARs setup + cd "${WORKSPACE}" + unset FAIL_ON_FAULT ; case '${{ env.STYLE_FAIL_ON_FAULT }}' in + ''|0|f|false|n|no|off) FAULT_TYPE=warning ;; + *) FAIL_ON_FAULT=true ; FAULT_TYPE=error ;; + esac; + FAULT_PREFIX=\$(echo "\${FAULT_TYPE}" | tr '[:lower:]' '[:upper:]') + # * determine sub-crate utility list + UTILITY_LIST="\$(./util/show-utils.sh --features ${{ matrix.job.features }})" + CARGO_UTILITY_LIST_OPTIONS="\$(for u in \${UTILITY_LIST}; do echo -n "-puu_\${u} "; done;)" + ## Info + # environment + echo "## environment" + echo "CI='${CI}'" + echo "REPO_NAME='${REPO_NAME}'" + echo "TEST_USER='${TEST_USER}'" + echo "WORKSPACE_PARENT='${WORKSPACE_PARENT}'" + echo "WORKSPACE='${WORKSPACE}'" + echo "FAULT_PREFIX='\${FAULT_PREFIX}'" + echo "UTILITY_LIST='\${UTILITY_LIST}'" + env | sort + # tooling info + echo "## tooling info" + cargo -V + rustc -V + # + # To ensure that files are cleaned up, we don't want to exit on error + set +e + unset FAULT + ## cargo fmt testing + echo "## cargo fmt testing" + # * convert any errors/warnings to GHA UI annotations; ref: + S=\$(cargo fmt -- --check) && printf "%s\n" "\$S" || { printf "%s\n" "\$S" ; printf "%s\n" "\$S" | sed -E -n -e "s/^Diff[[:space:]]+in[[:space:]]+\${PWD//\//\\\\/}\/(.*)[[:space:]]+at[[:space:]]+[^0-9]+([0-9]+).*\$/::\${FAULT_TYPE} file=\1,line=\2::\${FAULT_PREFIX}: \\\`cargo fmt\\\`: style violation (file:'\1', line:\2; use \\\`cargo fmt -- \"\1\"\\\`)/p" ; FAULT=true ; } + ## cargo clippy lint testing + if [ -z "\${FAULT}" ]; then + echo "## cargo clippy lint testing" + # * convert any warnings to GHA UI annotations; ref: + S=\$(cargo clippy --all-targets \${CARGO_UTILITY_LIST_OPTIONS} -- -D warnings 2>&1) && printf "%s\n" "\$S" || { printf "%s\n" "\$S" ; printf "%s" "\$S" | sed -E -n -e '/^error:/{' -e "N; s/^error:[[:space:]]+(.*)\\n[[:space:]]+-->[[:space:]]+(.*):([0-9]+):([0-9]+).*\$/::\${FAULT_TYPE} file=\2,line=\3,col=\4::\${FAULT_PREFIX}: \\\`cargo clippy\\\`: \1 (file:'\2', line:\3)/p;" -e '}' ; FAULT=true ; } + fi + # Clean to avoid to rsync back the files + cargo clean + if [ -n "\${FAIL_ON_FAULT}" ] && [ -n "\${FAULT}" ]; then exit 1 ; fi + EOF + + test: + name: Tests + runs-on: ubuntu-latest + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + job: + - { features: unix } + steps: + - uses: actions/checkout@v5 + with: + persist-credentials: false + - name: Prepare, build and test + uses: vmactions/openbsd-vm@v1 + with: + usesh: true + sync: rsync + copyback: false + mem: 4096 + # Install rust and build dependencies from OpenBSD packages (llvm provides libclang for bindgen) + prepare: pkg_add curl gmake sudo-- jq rust llvm-- + run: | + ## Prepare, build, and test + # implementation modelled after ref: + # * NOTE: All steps need to be run in this block, otherwise, we are operating back on the mac host + set -e + # + TEST_USER=tester + REPO_NAME=${GITHUB_WORKSPACE##*/} + WORKSPACE_PARENT="/home/runner/work/${REPO_NAME}" + WORKSPACE="${WORKSPACE_PARENT}/${REPO_NAME}" + # + useradd -m -G wheel ${TEST_USER} + chown -R ${TEST_USER}:wheel /root/ "${WORKSPACE_PARENT}"/ + whoami + # + # Further work needs to be done in a sudo as we are changing users + sudo -i -u ${TEST_USER} sh << EOF + set -e + whoami + # Rust is installed from packages, no need for rustup + # Set up PATH for cargo + export PATH="/usr/local/bin:$PATH" + # Install nextest + mkdir -p ~/.cargo/bin + # Note: nextest might not have OpenBSD builds, so we'll use regular cargo test + ## Info + # environment + echo "## environment" + echo "CI='${CI}'" + echo "REPO_NAME='${REPO_NAME}'" + echo "TEST_USER='${TEST_USER}'" + echo "WORKSPACE_PARENT='${WORKSPACE_PARENT}'" + echo "WORKSPACE='${WORKSPACE}'" + env | sort + # tooling info + echo "## tooling info" + cargo -V + rustc -V + # + # To ensure that files are cleaned up, we don't want to exit on error + set +e + cd "${WORKSPACE}" + unset FAULT + cargo build || FAULT=1 + export PATH=~/.cargo/bin:${PATH} + export RUST_BACKTRACE=1 + export CARGO_TERM_COLOR=always + # Use cargo test since nextest might not support OpenBSD + if (test -z "\$FAULT"); then cargo test --features '${{ matrix.job.features }}' || FAULT=1 ; fi + # There is no systemd-logind on OpenBSD, so test all features except feat_systemd_logind + if (test -z "\$FAULT"); then + UUCORE_FEATURES=\$(cargo metadata --format-version=1 --no-deps | jq -r '.packages[] | select(.name == "uucore") | .features | keys | .[]' | grep -v "feat_systemd_logind" | paste -s -d "," -) + cargo test --features "\$UUCORE_FEATURES" -p uucore || FAULT=1 + fi + # Test building with make + if (test -z "\$FAULT"); then make PROFILE=ci || FAULT=1 ; fi + # Clean to avoid to rsync back the files + cargo clean + if (test -n "\$FAULT"); then exit 1 ; fi + EOF From 2a8a7b36e3a72e4b9397a664680362518b0b96ed Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sat, 8 Nov 2025 21:03:40 +0100 Subject: [PATCH 003/182] silent some tests for openbsd for now --- tests/by-util/test_cp.rs | 24 ++++++++++++------------ tests/by-util/test_df.rs | 2 +- tests/by-util/test_hostname.rs | 4 ++-- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index aa91e4c4c..3c5b3242e 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -69,7 +69,7 @@ static TEST_NONEXISTENT_FILE: &str = "nonexistent_file.txt"; use uutests::util::compare_xattrs; /// Assert that mode, ownership, and permissions of two metadata objects match. -#[cfg(all(not(windows), not(target_os = "freebsd")))] +#[cfg(all(not(windows), not(target_os = "freebsd"), not(target_os = "openbsd")))] macro_rules! assert_metadata_eq { ($m1:expr, $m2:expr) => {{ assert_eq!($m1.mode(), $m2.mode(), "mode is different"); @@ -1553,7 +1553,7 @@ fn test_cp_parents_with_permissions_copy_file() { .arg(dir) .succeeds(); - #[cfg(all(unix, not(target_os = "freebsd")))] + #[cfg(all(unix, not(target_os = "freebsd"), not(target_os = "openbsd")))] { let p1_metadata = at.metadata("p1"); let p2_metadata = at.metadata("p1/p2"); @@ -1596,7 +1596,7 @@ fn test_cp_parents_with_permissions_copy_dir() { .arg(dir1) .succeeds(); - #[cfg(all(unix, not(target_os = "freebsd")))] + #[cfg(all(unix, not(target_os = "freebsd"), not(target_os = "openbsd")))] { let p1_metadata = at.metadata("p1"); let p2_metadata = at.metadata("p1/p2"); @@ -1641,7 +1641,7 @@ fn test_cp_preserve_no_args() { .arg("--preserve") .succeeds(); - #[cfg(all(unix, not(target_os = "freebsd")))] + #[cfg(all(unix, not(target_os = "freebsd"), not(target_os = "openbsd")))] { // Assert that the mode, ownership, and timestamps are preserved // NOTICE: the ownership is not modified on the src file, because that requires root permissions @@ -1669,7 +1669,7 @@ fn test_cp_preserve_no_args_before_opts() { .arg(dst_file) .succeeds(); - #[cfg(all(unix, not(target_os = "freebsd")))] + #[cfg(all(unix, not(target_os = "freebsd"), not(target_os = "openbsd")))] { // Assert that the mode, ownership, and timestamps are preserved // NOTICE: the ownership is not modified on the src file, because that requires root permissions @@ -1695,7 +1695,7 @@ fn test_cp_preserve_all() { // Copy ucmd.arg(src_file).arg(dst_file).arg(argument).succeeds(); - #[cfg(all(unix, not(target_os = "freebsd")))] + #[cfg(all(unix, not(target_os = "freebsd"), not(target_os = "openbsd")))] { // Assert that the mode, ownership, and timestamps are preserved // NOTICE: the ownership is not modified on the src file, because that requires root permissions @@ -3028,7 +3028,7 @@ fn test_copy_through_dangling_symlink_no_dereference_permissions() { assert!(at.symlink_exists("d2"), "symlink wasn't created"); // `-p` means `--preserve=mode,ownership,timestamps` - #[cfg(all(unix, not(target_os = "freebsd")))] + #[cfg(all(unix, not(target_os = "freebsd"), not(target_os = "openbsd")))] { let metadata1 = at.symlink_metadata("dangle"); let metadata2 = at.symlink_metadata("d2"); @@ -3749,7 +3749,7 @@ fn test_preserve_hardlink_attributes_in_directory() { // // A hard link should have the same inode as the target file. at.file_exists("dest/src/link"); - #[cfg(all(unix, not(target_os = "freebsd")))] + #[cfg(all(unix, not(target_os = "freebsd"), not(target_os = "openbsd")))] assert_eq!( at.metadata("dest/src/f").ino(), at.metadata("dest/src/link").ino() @@ -3765,7 +3765,7 @@ fn test_hard_link_file() { ucmd.args(&["-f", "--link", "src", "dest"]) .succeeds() .no_output(); - #[cfg(all(unix, not(target_os = "freebsd")))] + #[cfg(all(unix, not(target_os = "freebsd"), not(target_os = "openbsd")))] assert_eq!(at.metadata("src").ino(), at.metadata("dest").ino()); } @@ -4069,7 +4069,7 @@ fn test_cp_dest_no_permissions() { } #[test] -#[cfg(all(unix, not(target_os = "freebsd")))] +#[cfg(all(unix, not(target_os = "freebsd"), not(target_os = "openbsd")))] fn test_cp_attributes_only() { let (at, mut ucmd) = at_and_ucmd!(); let a = "file_a"; @@ -6537,7 +6537,7 @@ fn test_cp_preserve_selinux() { selinux_perm_dest ); - #[cfg(all(unix, not(target_os = "freebsd")))] + #[cfg(all(unix, not(target_os = "freebsd"), not(target_os = "openbsd")))] { // Assert that the mode, ownership, and timestamps are preserved // NOTICE: the ownership is not modified on the src file, because that requires root permissions @@ -6949,7 +6949,7 @@ fn test_cp_current_directory_verbose() { // Test copying current directory (.) with preserve attributes. // This ensures attributes are preserved when copying the current directory. #[test] -#[cfg(all(not(windows), not(target_os = "freebsd")))] +#[cfg(all(not(windows), not(target_os = "freebsd"), not(target_os = "openbsd")))] fn test_cp_current_directory_preserve_attributes() { use filetime::FileTime; use std::os::unix::prelude::MetadataExt; diff --git a/tests/by-util/test_df.rs b/tests/by-util/test_df.rs index 149148699..7973cffd1 100644 --- a/tests/by-util/test_df.rs +++ b/tests/by-util/test_df.rs @@ -326,7 +326,7 @@ fn test_type_option() { } #[test] -#[cfg(not(any(target_os = "freebsd", target_os = "windows")))] // FIXME: fix test for FreeBSD & Win +#[cfg(not(any(target_os = "freebsd", target_os = "windows", target_os = "openbsd")))] // FIXME: fix test for FreeBSD, OpenBSD & Win #[cfg(not(feature = "feat_selinux"))] fn test_type_option_with_file() { let fs_type = new_ucmd!() diff --git a/tests/by-util/test_hostname.rs b/tests/by-util/test_hostname.rs index e58c236a5..aedf7d015 100644 --- a/tests/by-util/test_hostname.rs +++ b/tests/by-util/test_hostname.rs @@ -14,8 +14,8 @@ fn test_hostname() { assert!(ls_default_res.stdout().len() >= ls_domain_res.stdout().len()); } -// FixME: fails for "MacOS" => "failed to lookup address information" -#[cfg(not(target_os = "macos"))] +// FixME: fails for "MacOS" and "OpenBSD" => "failed to lookup address information" +#[cfg(not(any(target_os = "macos", target_os = "openbsd")))] #[test] fn test_hostname_ip() { let result = new_ucmd!().arg("-i").succeeds(); From ab6ec2fd02f5c5fdd40fb0addf6a36810049d8a7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 9 Nov 2025 21:49:52 +0000 Subject: [PATCH 004/182] chore(deps): update rust crate parse_datetime to v0.13.2 --- Cargo.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 05501281b..12f788c83 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1664,7 +1664,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde_core", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -1962,7 +1962,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]] @@ -2132,9 +2132,9 @@ dependencies = [ [[package]] name = "parse_datetime" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77d45119ed61100f40b2389d8ed12e51ec869046d4279afbb5a7c73a4733be36" +checksum = "e4955561bc7aa4c40afcfd2a8c34297b13164ae9ac3b30ac348737befdc98e4c" dependencies = [ "jiff", "num-traits", @@ -2567,7 +2567,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.11.0", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -2867,7 +2867,7 @@ dependencies = [ "getrandom 0.3.3", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -4556,7 +4556,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 2b82d891ac5acf199b4c90cc6622816bad225242 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Mon, 10 Nov 2025 15:46:46 +0100 Subject: [PATCH 005/182] gnu version: extract the info from uutils/util/build-gnu.sh to avoid duplication --- .github/workflows/GnuTests.yml | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/.github/workflows/GnuTests.yml b/.github/workflows/GnuTests.yml index 1feb49677..f82bbcabd 100644 --- a/.github/workflows/GnuTests.yml +++ b/.github/workflows/GnuTests.yml @@ -29,7 +29,6 @@ env: TEST_ROOT_FULL_SUMMARY_FILE: 'gnu-root-full-result.json' TEST_SELINUX_FULL_SUMMARY_FILE: 'selinux-gnu-full-result.json' TEST_SELINUX_ROOT_FULL_SUMMARY_FILE: 'selinux-root-gnu-full-result.json' - REPO_GNU_REF: "v9.8" jobs: native: @@ -42,6 +41,16 @@ jobs: with: path: 'uutils' persist-credentials: false + - name: Extract GNU version from build-gnu.sh + id: gnu-version + run: | + GNU_VERSION=$(grep '^release_tag_GNU=' uutils/util/build-gnu.sh | cut -d'"' -f2) + if [ -z "$GNU_VERSION" ]; then + echo "Error: Failed to extract GNU version from build-gnu.sh" + exit 1 + fi + echo "REPO_GNU_REF=${GNU_VERSION}" >> $GITHUB_ENV + echo "Extracted GNU version: ${GNU_VERSION}" - uses: dtolnay/rust-toolchain@master with: toolchain: stable @@ -164,6 +173,16 @@ jobs: with: path: 'uutils' persist-credentials: false + - name: Extract GNU version from build-gnu.sh + id: gnu-version-selinux + run: | + GNU_VERSION=$(grep '^release_tag_GNU=' uutils/util/build-gnu.sh | cut -d'"' -f2) + if [ -z "$GNU_VERSION" ]; then + echo "Error: Failed to extract GNU version from build-gnu.sh" + exit 1 + fi + echo "REPO_GNU_REF=${GNU_VERSION}" >> $GITHUB_ENV + echo "Extracted GNU version: ${GNU_VERSION}" - uses: dtolnay/rust-toolchain@master with: toolchain: stable From 713d1e6fc26beacd328d7130c2055bc23734b516 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Mon, 10 Nov 2025 15:47:48 +0100 Subject: [PATCH 006/182] upgrade to GNU coreutils 9.9 as ref --- util/build-gnu.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util/build-gnu.sh b/util/build-gnu.sh index 734088252..394bbf1a0 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -70,7 +70,7 @@ fi ### -release_tag_GNU="v9.8" +release_tag_GNU="v9.9" # check if the GNU coreutils has been cloned, if not print instructions # note: the ${path_GNU} might already exist, so we check for the .git directory From 7371b5bc28f2f593a5b5c1fae530c9efa5fe5fe9 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Mon, 10 Nov 2025 16:34:19 +0100 Subject: [PATCH 007/182] gnu tests: adjust the numfmt path --- util/build-gnu.sh | 4 ++-- util/why-error.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/util/build-gnu.sh b/util/build-gnu.sh index 394bbf1a0..aeaceda0e 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -309,10 +309,10 @@ sed -i -e "s/ginstall: creating directory/install: creating directory/g" tests/i # GNU doesn't support padding < -LONG_MAX # disable this test case # Use GNU sed because option -z is not available on BSD sed -"${SED}" -i -Ez "s/\n([^\n#]*pad-3\.2[^\n]*)\n([^\n]*)\n([^\n]*)/\n# uutils\/numfmt supports padding = LONG_MIN\n#\1\n#\2\n#\3/" tests/misc/numfmt.pl +"${SED}" -i -Ez "s/\n([^\n#]*pad-3\.2[^\n]*)\n([^\n]*)\n([^\n]*)/\n# uutils\/numfmt supports padding = LONG_MIN\n#\1\n#\2\n#\3/" tests/numfmt/numfmt.pl # Update the GNU error message to match the one generated by clap -sed -i -e "s/\$prog: multiple field specifications/error: the argument '--field ' cannot be used multiple times\n\nUsage: numfmt [OPTION]... [NUMBER]...\n\nFor more information, try '--help'./g" tests/misc/numfmt.pl +sed -i -e "s/\$prog: multiple field specifications/error: the argument '--field ' cannot be used multiple times\n\nUsage: numfmt [OPTION]... [NUMBER]...\n\nFor more information, try '--help'./g" tests/numfmt/numfmt.pl sed -i -e "s/Try 'mv --help' for more information/For more information, try '--help'/g" -e "s/mv: missing file operand/error: the following required arguments were not provided:\n ...\n\nUsage: mv [OPTION]... [-T] SOURCE DEST\n mv [OPTION]... SOURCE... DIRECTORY\n mv [OPTION]... -t DIRECTORY SOURCE...\n/g" -e "s/mv: missing destination file operand after 'no-file'/error: The argument '...' requires at least 2 values, but only 1 was provided\n\nUsage: mv [OPTION]... [-T] SOURCE DEST\n mv [OPTION]... SOURCE... DIRECTORY\n mv [OPTION]... -t DIRECTORY SOURCE...\n/g" tests/mv/diag.sh # our error message is better diff --git a/util/why-error.md b/util/why-error.md index 0fdff867a..ac1e10ce6 100644 --- a/util/why-error.md +++ b/util/why-error.md @@ -26,7 +26,7 @@ This file documents why some tests are failing: * gnu/tests/misc/close-stdout.sh * gnu/tests/misc/comm.pl * gnu/tests/misc/nohup.sh -* gnu/tests/misc/numfmt.pl - https://github.com/uutils/coreutils/issues/7219 / https://github.com/uutils/coreutils/issues/7221 +* gnu/tests/numfmt/numfmt.pl - https://github.com/uutils/coreutils/issues/7219 / https://github.com/uutils/coreutils/issues/7221 * gnu/tests/misc/stdbuf.sh - https://github.com/uutils/coreutils/issues/7072 * gnu/tests/misc/tee.sh - https://github.com/uutils/coreutils/issues/7073 * gnu/tests/misc/time-style.sh From 364d9e9dffab348e32fdf12819c1c5bc39775f3d Mon Sep 17 00:00:00 2001 From: karanabe <152078880+karanabe@users.noreply.github.com> Date: Tue, 11 Nov 2025 03:13:51 +0900 Subject: [PATCH 008/182] basenc: Fix basenc.pl GNU-compat tests pass (#9203) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(basenc): align base32 decode with GNU * Add GNU-style basenc base32 tests * Expand basenc base32 tests and simplify failures Adds the GNU-style auto-padding/truncated cases to tests/by-util/test_basenc.rs and rewrites the failure assertions to use the chained fails().stdout_*(…).stderr_is(…) style for clarity. * Restore GNU expectations for b32h_5 and b32h_6 Updates util/build-gnu.sh to stop forcing those two basenc tests to expect empty stdout, so the GNU suite again checks for the leaked five bytes before failure. * Allow base32 decoder to auto-pad truncated blocks Introduce PadResult, trim/pad incomplete base32 chunks, emit decoded prefixes, and still return error: invalid input in line with GNU basenc. --- src/uu/base32/src/base_common.rs | 201 ++++++++++++++---------- src/uucore/src/lib/features/encoding.rs | 95 +++++++++++ tests/by-util/test_basenc.rs | 58 +++++++ util/build-gnu.sh | 2 +- 4 files changed, 276 insertions(+), 80 deletions(-) diff --git a/src/uu/base32/src/base_common.rs b/src/uu/base32/src/base_common.rs index 96d28e189..65cadc7c3 100644 --- a/src/uu/base32/src/base_common.rs +++ b/src/uu/base32/src/base_common.rs @@ -8,11 +8,11 @@ use clap::{Arg, ArgAction, Command}; use std::ffi::OsString; use std::fs::File; -use std::io::{self, ErrorKind, Read, Seek}; +use std::io::{self, ErrorKind, Read, Seek, Write}; use std::path::{Path, PathBuf}; use uucore::display::Quotable; use uucore::encoding::{ - BASE2LSBF, BASE2MSBF, Base58Wrapper, Base64SimdWrapper, EncodingWrapper, Format, + BASE2LSBF, BASE2MSBF, Base32Wrapper, Base58Wrapper, Base64SimdWrapper, EncodingWrapper, Format, SupportsFastDecodeAndEncode, Z85Wrapper, for_base_common::{BASE32, BASE32HEX, BASE64URL, HEXUPPER_PERMISSIVE}, }; @@ -193,7 +193,7 @@ pub fn handle_input(input: &mut R, format: Format, config: Confi let supports_fast_decode_and_encode_ref = supports_fast_decode_and_encode.as_ref(); let mut stdout_lock = io::stdout().lock(); - if config.decode { + let result = if config.decode { fast_decode::fast_decode( read, &mut stdout_lock, @@ -207,6 +207,14 @@ pub fn handle_input(input: &mut R, format: Format, config: Confi supports_fast_decode_and_encode_ref, config.wrap_cols, ) + }; + + // Ensure any pending stdout buffer is flushed even if decoding failed; GNU basenc + // keeps already-decoded bytes visible before reporting the error. + match (result, stdout_lock.flush()) { + (res, Ok(())) => res, + (Ok(_), Err(err)) => Err(err.into()), + (Err(original), Err(_)) => Err(original), } } @@ -247,14 +255,14 @@ pub fn get_supports_fast_decode_and_encode( // spell-checker:disable-next-line b"01", )), - Format::Base32 => Box::from(EncodingWrapper::new( + Format::Base32 => Box::from(Base32Wrapper::new( BASE32, BASE32_VALID_DECODING_MULTIPLE, BASE32_UNPADDED_MULTIPLE, // spell-checker:disable-next-line b"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567=", )), - Format::Base32Hex => Box::from(EncodingWrapper::new( + Format::Base32Hex => Box::from(Base32Wrapper::new( BASE32HEX, BASE32_VALID_DECODING_MULTIPLE, BASE32_UNPADDED_MULTIPLE, @@ -502,43 +510,21 @@ pub mod fast_encode { pub mod fast_decode { use std::io::{self, Write}; - use uucore::{encoding::SupportsFastDecodeAndEncode, error::UResult}; + use uucore::{ + encoding::SupportsFastDecodeAndEncode, + error::{UResult, USimpleError}, + }; // Start of helper functions - fn alphabet_to_table(alphabet: &[u8], ignore_garbage: bool) -> [bool; 256] { - // If `ignore_garbage` is enabled, all characters outside the alphabet are ignored - // If it is not enabled, only '\n' and '\r' are ignored - if ignore_garbage { - // Note: "false" here - let mut table = [false; 256]; + fn alphabet_lookup(alphabet: &[u8]) -> [bool; 256] { + // Precompute O(1) membership checks so we can validate every byte before decoding. + let mut table = [false; 256]; - // Pass through no characters except those in the alphabet - for ue in alphabet { - let us = usize::from(*ue); - - // Should not have been set yet - assert!(!table[us]); - - table[us] = true; - } - - table - } else { - // Note: "true" here - let mut table = [true; 256]; - - // Pass through all characters except '\n' and '\r' - for ue in [b'\n', b'\r'] { - let us = usize::from(ue); - - // Should not have been set yet - assert!(table[us]); - - table[us] = false; - } - - table + for &byte in alphabet { + table[usize::from(byte)] = true; } + + table } fn decode_in_chunks_to_buffer( @@ -553,11 +539,44 @@ pub mod fast_decode { fn write_to_output(decoded_buffer: &mut Vec, output: &mut dyn Write) -> io::Result<()> { // Write all data in `decoded_buffer` to `output` output.write_all(decoded_buffer.as_slice())?; + output.flush()?; decoded_buffer.clear(); Ok(()) } + + fn flush_ready_chunks( + buffer: &mut Vec, + block_limit: usize, + valid_multiple: usize, + supports_fast_decode_and_encode: &dyn SupportsFastDecodeAndEncode, + decoded_buffer: &mut Vec, + output: &mut dyn Write, + ) -> UResult<()> { + // While at least one full decode block is buffered, keep draining + // it and never yield more than block_limit per chunk. + while buffer.len() >= valid_multiple { + let take = buffer.len().min(block_limit); + let aligned_take = take - (take % valid_multiple); + + if aligned_take < valid_multiple { + break; + } + + decode_in_chunks_to_buffer( + supports_fast_decode_and_encode, + &buffer[..aligned_take], + decoded_buffer, + )?; + + write_to_output(decoded_buffer, output)?; + + buffer.drain(..aligned_take); + } + + Ok(()) + } // End of helper functions pub fn fast_decode( @@ -569,22 +588,12 @@ pub mod fast_decode { const DECODE_IN_CHUNKS_OF_SIZE_MULTIPLE: usize = 1_024; let alphabet = supports_fast_decode_and_encode.alphabet(); - let decode_in_chunks_of_size = supports_fast_decode_and_encode.valid_decoding_multiple() - * DECODE_IN_CHUNKS_OF_SIZE_MULTIPLE; + let alphabet_table = alphabet_lookup(alphabet); + let valid_multiple = supports_fast_decode_and_encode.valid_decoding_multiple(); + let decode_in_chunks_of_size = valid_multiple * DECODE_IN_CHUNKS_OF_SIZE_MULTIPLE; assert!(decode_in_chunks_of_size > 0); - - // Note that it's not worth using "data-encoding"'s ignore functionality if `ignore_garbage` is true, because - // "data-encoding"'s ignore functionality cannot discard non-ASCII bytes. The data has to be filtered before - // passing it to "data-encoding", so there is no point in doing any filtering in "data-encoding". This also - // allows execution to stay on the happy path in "data-encoding": - // https://github.com/ia0/data-encoding/blob/4f42ad7ef242f6d243e4de90cd1b46a57690d00e/lib/src/lib.rs#L754-L756 - // It is also not worth using "data-encoding"'s ignore functionality when `ignore_garbage` is - // false. - // Note that the alphabet constants above already include the padding characters - // TODO - // Precompute this - let table = alphabet_to_table(alphabet, ignore_garbage); + assert!(valid_multiple > 0); // Start of buffers @@ -595,35 +604,69 @@ pub mod fast_decode { let mut buffer = Vec::with_capacity(decode_in_chunks_of_size); - input - .iter() - .filter(|ch| table[usize::from(**ch)]) - .for_each(|ch| { - buffer.push(*ch); - // How many bytes to steal from `read_buffer` to get - // `leftover_buffer` to the right size - if buffer.len() == decode_in_chunks_of_size { - assert_eq!(decode_in_chunks_of_size, buffer.len()); - // Decode data in chunks, then place it in `decoded_buffer` - decode_in_chunks_to_buffer( - supports_fast_decode_and_encode, - &buffer, - &mut decoded_buffer, - ) - .unwrap(); - // Write all data in `decoded_buffer` to `output` - write_to_output(&mut decoded_buffer, output).unwrap(); - buffer.clear(); - } - }); - // Cleanup - // `input` has finished producing data, so the data remaining in the buffers needs to be decoded and printed - { - // Decode all remaining encoded bytes, placing them in `decoded_buffer` - supports_fast_decode_and_encode.decode_into_vec(&buffer, &mut decoded_buffer)?; + let supports_partial_decode = supports_fast_decode_and_encode.supports_partial_decode(); - // Write all data in `decoded_buffer` to `output` + for &byte in &input { + if byte == b'\n' || byte == b'\r' { + continue; + } + + if alphabet_table[usize::from(byte)] { + buffer.push(byte); + } else if ignore_garbage { + continue; + } else { + return Err(USimpleError::new(1, "error: invalid input".to_owned())); + } + + if supports_partial_decode { + flush_ready_chunks( + &mut buffer, + decode_in_chunks_of_size, + valid_multiple, + supports_fast_decode_and_encode, + &mut decoded_buffer, + output, + )?; + } else if buffer.len() == decode_in_chunks_of_size { + decode_in_chunks_to_buffer( + supports_fast_decode_and_encode, + &buffer, + &mut decoded_buffer, + )?; + write_to_output(&mut decoded_buffer, output)?; + buffer.clear(); + } + } + + if supports_partial_decode { + flush_ready_chunks( + &mut buffer, + decode_in_chunks_of_size, + valid_multiple, + supports_fast_decode_and_encode, + &mut decoded_buffer, + output, + )?; + } + + if !buffer.is_empty() { + let mut owned_chunk: Option> = None; + let mut had_invalid_tail = false; + + if let Some(pad_result) = supports_fast_decode_and_encode.pad_remainder(&buffer) { + had_invalid_tail = pad_result.had_invalid_tail; + owned_chunk = Some(pad_result.chunk); + } + + let final_chunk = owned_chunk.as_deref().unwrap_or(&buffer); + + supports_fast_decode_and_encode.decode_into_vec(final_chunk, &mut decoded_buffer)?; write_to_output(&mut decoded_buffer, output)?; + + if had_invalid_tail { + return Err(USimpleError::new(1, "error: invalid input".to_owned())); + } } Ok(()) diff --git a/src/uucore/src/lib/features/encoding.rs b/src/uucore/src/lib/features/encoding.rs index 6c6261c2c..2f7caae2b 100644 --- a/src/uucore/src/lib/features/encoding.rs +++ b/src/uucore/src/lib/features/encoding.rs @@ -214,6 +214,11 @@ impl EncodingWrapper { } } +pub struct PadResult { + pub chunk: Vec, + pub had_invalid_tail: bool, +} + pub trait SupportsFastDecodeAndEncode { /// Returns the list of characters used by this encoding fn alphabet(&self) -> &'static [u8]; @@ -245,6 +250,19 @@ pub trait SupportsFastDecodeAndEncode { /// /// The decoding performed by `fast_decode` depends on this number being correct. fn valid_decoding_multiple(&self) -> usize; + + /// Whether the decoder can flush partial chunks (multiples of `valid_decoding_multiple`) + /// before seeing the full input. Defaults to `false` for encodings that must consume the + /// entire input (e.g. base58). + fn supports_partial_decode(&self) -> bool { + false + } + + /// Gives encoding-specific logic a chance to pad a trailing, non-empty remainder + /// before the final decode attempt. The default implementation opts out. + fn pad_remainder(&self, _remainder: &[u8]) -> Option { + None + } } impl SupportsFastDecodeAndEncode for Base58Wrapper { @@ -504,3 +522,80 @@ impl SupportsFastDecodeAndEncode for EncodingWrapper { self.unpadded_multiple } } + +pub struct Base32Wrapper { + inner: EncodingWrapper, +} + +impl Base32Wrapper { + pub fn new( + encoding: Encoding, + valid_decoding_multiple: usize, + unpadded_multiple: usize, + alphabet: &'static [u8], + ) -> Self { + Self { + inner: EncodingWrapper::new( + encoding, + valid_decoding_multiple, + unpadded_multiple, + alphabet, + ), + } + } +} + +impl SupportsFastDecodeAndEncode for Base32Wrapper { + fn alphabet(&self) -> &'static [u8] { + self.inner.alphabet() + } + + fn decode_into_vec(&self, input: &[u8], output: &mut Vec) -> UResult<()> { + self.inner.decode_into_vec(input, output) + } + + fn encode_to_vec_deque(&self, input: &[u8], output: &mut VecDeque) -> UResult<()> { + self.inner.encode_to_vec_deque(input, output) + } + + fn unpadded_multiple(&self) -> usize { + self.inner.unpadded_multiple() + } + + fn valid_decoding_multiple(&self) -> usize { + self.inner.valid_decoding_multiple() + } + + fn pad_remainder(&self, remainder: &[u8]) -> Option { + if remainder.is_empty() || remainder.contains(&b'=') { + return None; + } + + const VALID_REMAINDERS: [usize; 4] = [2, 4, 5, 7]; + + let mut len = remainder.len(); + let mut trimmed = false; + + while len > 0 && !VALID_REMAINDERS.contains(&len) { + len -= 1; + trimmed = true; + } + + if len == 0 { + return None; + } + + let mut padded = remainder[..len].to_vec(); + let missing = self.valid_decoding_multiple() - padded.len(); + padded.extend(std::iter::repeat_n(b'=', missing)); + + Some(PadResult { + chunk: padded, + had_invalid_tail: trimmed, + }) + } + + fn supports_partial_decode(&self) -> bool { + true + } +} diff --git a/tests/by-util/test_basenc.rs b/tests/by-util/test_basenc.rs index f02de772b..a3c92b885 100644 --- a/tests/by-util/test_basenc.rs +++ b/tests/by-util/test_basenc.rs @@ -4,6 +4,7 @@ // file that was distributed with this source code. // spell-checker: ignore (encodings) lsbf msbf +// spell-checker: ignore autopad MFRGG MFRGGZDF abcdeabc baddecode CPNMUO use uutests::{at_and_ucmd, new_ucmd}; @@ -112,6 +113,63 @@ fn test_base32hex_decode() { .stdout_only("nice>base?"); } +#[test] +fn test_base32_autopad_short_quantum() { + new_ucmd!() + .args(&["--base32", "--decode"]) + .pipe_in("MFRGG") + .succeeds() + .stdout_only("abc"); +} + +#[test] +fn test_base32_autopad_multiline_stream() { + new_ucmd!() + .args(&["--base32", "--decode"]) + .pipe_in("MFRGGZDF\nMFRGG") + .succeeds() + .stdout_only("abcdeabc"); +} + +#[test] +fn test_base32_baddecode_keeps_prefix() { + new_ucmd!() + .args(&["--base32", "--decode"]) + .pipe_in("MFRGGZDF=") + .fails() + .stdout_is("abcde") + .stderr_is("basenc: error: invalid input\n"); +} + +#[test] +fn test_base32hex_autopad_short_quantum() { + new_ucmd!() + .args(&["--base32hex", "--decode"]) + .pipe_in("C5H66") + .succeeds() + .stdout_only("abc"); +} + +#[test] +fn test_base32hex_rejects_trailing_garbage() { + new_ucmd!() + .args(&["--base32hex", "-d"]) + .pipe_in("VNC0FKD5W") + .fails() + .stdout_is_bytes(b"\xFD\xD8\x07\xD1\xA5") + .stderr_is("basenc: error: invalid input\n"); +} + +#[test] +fn test_base32hex_truncated_block_keeps_prefix() { + new_ucmd!() + .args(&["--base32hex", "-d"]) + .pipe_in("CPNMUO") + .fails() + .stdout_is_bytes(b"foo") + .stderr_is("basenc: error: invalid input\n"); +} + #[test] fn test_base16() { new_ucmd!() diff --git a/util/build-gnu.sh b/util/build-gnu.sh index aeaceda0e..99b921e69 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -268,7 +268,7 @@ sed -i -e "s|invalid suffix in --pages argument|invalid --pages argument|" \ # When decoding an invalid base32/64 string, gnu writes everything it was able to decode until # it hit the decode error, while we don't write anything if the input is invalid. sed -i "s/\(baddecode.*OUT=>\"\).*\"/\1\"/g" tests/basenc/base64.pl -sed -i "s/\(\(b2[ml]_[69]\|b32h_[56]\|z85_8\|z85_35\).*OUT=>\)[^}]*\(.*\)/\1\"\"\3/g" tests/basenc/basenc.pl +sed -i "s/\(\(b2[ml]_[69]\|z85_8\|z85_35\).*OUT=>\)[^}]*\(.*\)/\1\"\"\3/g" tests/basenc/basenc.pl # add "error: " to the expected error message sed -i "s/\$prog: invalid input/\$prog: error: invalid input/g" tests/basenc/basenc.pl From 91931bcf737b004c2b2d2c9fd5811fc045c4231f Mon Sep 17 00:00:00 2001 From: E <79379754+oech3@users.noreply.github.com> Date: Tue, 11 Nov 2025 04:30:51 +0900 Subject: [PATCH 009/182] CICD.yml: split PROFILE= from CARGOFLAGS (#9219) --- .github/workflows/CICD.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index 529ac2091..e0a4a9141 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -300,7 +300,7 @@ jobs: mv -t target/ target.cache/release 2>/dev/null || true - name: "`make nextest`" shell: bash - run: make nextest CARGOFLAGS="--profile ci --hide-progress-bar" + run: make nextest PROFILE=ci CARGOFLAGS="--hide-progress-bar" env: RUST_BACKTRACE: "1" - name: "`make install COMPLETIONS=n MANPAGES=n LOCALES=n`" From 9f4fa5bad2f5fe9337c04f80263e6674965b5b65 Mon Sep 17 00:00:00 2001 From: WaterWhisperer Date: Tue, 11 Nov 2025 03:45:05 +0800 Subject: [PATCH 010/182] uucore: embed system locale on cargo install (#8604) --- .github/workflows/l10n.yml | 130 +++++++++++++- src/uucore/build.rs | 359 +++++++++++++++++++++++++++++-------- 2 files changed, 416 insertions(+), 73 deletions(-) diff --git a/.github/workflows/l10n.yml b/.github/workflows/l10n.yml index f5f1871f7..1d244c6fb 100644 --- a/.github/workflows/l10n.yml +++ b/.github/workflows/l10n.yml @@ -1245,10 +1245,138 @@ jobs: exit 1 fi + l10n_locale_embedding_cargo_install: + name: L10n/Locale Embedding - Cargo Install + runs-on: ubuntu-latest + env: + SCCACHE_GHA_ENABLED: "true" + RUSTC_WRAPPER: "sccache" + steps: + - uses: actions/checkout@v5 + with: + persist-credentials: false + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + with: + key: cargo-install-locale-embedding + - name: Run sccache-cache + uses: mozilla-actions/sccache-action@v0.0.9 + - name: Install prerequisites + run: | + sudo apt-get -y update + sudo apt-get -y install libselinux1-dev locales + # Generate French locale for testing + sudo locale-gen --keep-existing fr_FR.UTF-8 + locale -a | grep -i fr || exit 1 + + - name: Test English locale embedding (default) + run: | + export LANG=en_US.UTF-8 + export LC_ALL=en_US.UTF-8 + + echo "Building uu_yes with LANG=$LANG" + cargo build --package uu_yes --release + + # Find the generated embedded_locales.rs + locale_file=$(find target/release/build -path "*/uu_yes-*/out/embedded_locales.rs" -o -path "*/uucore-*/out/embedded_locales.rs" | head -1) + if [ -z "$locale_file" ]; then + echo "ERROR: Could not find embedded_locales.rs" + exit 1 + fi + + echo "Found embedded_locales.rs at: $locale_file" + echo "Checking embedded locales..." + + # Should contain en-US + if grep -q 'yes/en-US\.ftl' "$locale_file" || grep -q 'uucore/en-US\.ftl' "$locale_file"; then + echo "✓ Found en-US locale (fallback)" + else + echo "✗ ERROR: en-US locale not found" + exit 1 + fi + + # Should NOT contain fr-FR when building with en_US.UTF-8 + if grep -q 'yes/fr-FR\.ftl' "$locale_file" || grep -q 'uucore/fr-FR\.ftl' "$locale_file"; then + echo "✗ ERROR: Unexpectedly found fr-FR locale when LANG=en_US.UTF-8" + exit 1 + else + echo "✓ Correctly omitted fr-FR locale" + fi + + echo "✓ SUCCESS: English locale embedding working correctly" + + - name: Test French locale embedding (system locale) + run: | + export LANG=fr_FR.UTF-8 + export LC_ALL=fr_FR.UTF-8 + + # Clean previous build to ensure fresh compile + cargo clean -p uu_yes + cargo clean -p uucore + + echo "Building uu_yes with LANG=$LANG" + cargo build --package uu_yes --release + + # Find the generated embedded_locales.rs + locale_file=$(find target/release/build -path "*/uu_yes-*/out/embedded_locales.rs" -o -path "*/uucore-*/out/embedded_locales.rs" | head -1) + if [ -z "$locale_file" ]; then + echo "ERROR: Could not find embedded_locales.rs" + exit 1 + fi + + echo "Found embedded_locales.rs at: $locale_file" + echo "Checking embedded locales..." + + # Should contain en-US (fallback) + if grep -q 'yes/en-US\.ftl' "$locale_file" || grep -q 'uucore/en-US\.ftl' "$locale_file"; then + echo "✓ Found en-US locale (fallback)" + else + echo "✗ ERROR: en-US locale not found" + exit 1 + fi + + # Should contain fr-FR when building with fr_FR.UTF-8 + if grep -q 'yes/fr-FR\.ftl' "$locale_file" || grep -q 'uucore/fr-FR\.ftl' "$locale_file"; then + echo "✓ Found fr-FR locale (system locale from LANG)" + else + echo "Note: fr-FR locale not found - this is expected if French translation doesn't exist yet" + echo "::notice::French locale for 'yes' utility may not be available" + fi + + echo "✓ SUCCESS: System locale detection working correctly" + + - name: Test locale count is reasonable + run: | + export LANG=fr_FR.UTF-8 + cargo clean -p uu_yes + cargo build --package uu_yes --release + + locale_file=$(find target/release/build -path "*/uucore-*/out/embedded_locales.rs" | head -1) + if [ -z "$locale_file" ]; then + echo "ERROR: Could not find uucore embedded_locales.rs" + exit 1 + fi + + # Count embedded locales (should be en-US + system locale, not all locales) + locale_count=$(grep -c '/en-US\.ftl\|/fr-FR\.ftl' "$locale_file" || echo "0") + echo "uu_yes has $locale_count embedded locale entries for yes utility" + + # For a single utility build, should have minimal locales (en-US + optionally system locale) + # Not the full multicall set + total_match_count=$(grep -c '=> Some(r###' "$locale_file" || echo "0") + echo "Total embedded entries: $total_match_count" + + if [ "$total_match_count" -le 10 ]; then + echo "✓ SUCCESS: Locale embedding is targeted ($total_match_count entries)" + else + echo "::warning::More locales than expected ($total_match_count entries)" + echo "This might be expected for utility + uucore locales" + fi + l10n_locale_embedding_regression_test: name: L10n/Locale Embedding Regression Test runs-on: ubuntu-latest - needs: [l10n_locale_embedding_cat, l10n_locale_embedding_ls, l10n_locale_embedding_multicall] + needs: [l10n_locale_embedding_cat, l10n_locale_embedding_ls, l10n_locale_embedding_multicall, l10n_locale_embedding_cargo_install] steps: - name: All locale embedding tests passed run: echo "✓ All locale embedding tests passed successfully" diff --git a/src/uucore/build.rs b/src/uucore/build.rs index d5637ef3f..f79b3922b 100644 --- a/src/uucore/build.rs +++ b/src/uucore/build.rs @@ -31,15 +31,21 @@ pub fn main() -> Result<(), Box> { // Try to detect if we're building for a specific utility by checking build configuration // This attempts to identify individual utility builds vs multicall binary builds let target_utility = detect_target_utility(); + let locales_to_embed = get_locales_to_embed(); match target_utility { Some(util_name) => { // Embed only the specific utility's locale (cat.ftl for cat for example) - embed_single_utility_locale(&mut embedded_file, &project_root()?, &util_name)?; + embed_single_utility_locale( + &mut embedded_file, + &project_root()?, + &util_name, + &locales_to_embed, + )?; } None => { // Embed all utility locales (multicall binary or fallback) - embed_all_utility_locales(&mut embedded_file, &project_root()?)?; + embed_all_utility_locales(&mut embedded_file, &project_root()?, &locales_to_embed)?; } } @@ -118,38 +124,20 @@ fn embed_single_utility_locale( embedded_file: &mut std::fs::File, project_root: &Path, util_name: &str, + locales_to_embed: &(String, Option), ) -> Result<(), Box> { - use std::fs; - - // Embed the specific utility's locale - let locale_path = project_root - .join("src/uu") - .join(util_name) - .join("locales/en-US.ftl"); - - if locale_path.exists() { - let content = fs::read_to_string(&locale_path)?; - writeln!(embedded_file, " // Locale for {util_name}")?; - writeln!( - embedded_file, - " \"{util_name}/en-US.ftl\" => Some(r###\"{content}\"###)," - )?; - - // Tell Cargo to rerun if this file changes - println!("cargo:rerun-if-changed={}", locale_path.display()); - } + // Embed utility-specific locales + embed_component_locales(embedded_file, locales_to_embed, util_name, |locale| { + project_root + .join("src/uu") + .join(util_name) + .join(format!("locales/{locale}.ftl")) + })?; // Always embed uucore locale file if it exists - let uucore_locale_path = project_root.join("src/uucore/locales/en-US.ftl"); - if uucore_locale_path.exists() { - let content = fs::read_to_string(&uucore_locale_path)?; - writeln!(embedded_file, " // Common uucore locale")?; - writeln!( - embedded_file, - " \"uucore/en-US.ftl\" => Some(r###\"{content}\"###)," - )?; - println!("cargo:rerun-if-changed={}", uucore_locale_path.display()); - } + embed_component_locales(embedded_file, locales_to_embed, "uucore", |locale| { + project_root.join(format!("src/uucore/locales/{locale}.ftl")) + })?; Ok(()) } @@ -158,6 +146,7 @@ fn embed_single_utility_locale( fn embed_all_utility_locales( embedded_file: &mut std::fs::File, project_root: &Path, + locales_to_embed: &(String, Option), ) -> Result<(), Box> { use std::fs; @@ -166,7 +155,7 @@ fn embed_all_utility_locales( if !src_uu_dir.exists() { // When src/uu doesn't exist (e.g., standalone uucore from crates.io), // embed a static list of utility locales that are commonly used - embed_static_utility_locales(embedded_file)?; + embed_static_utility_locales(embedded_file, locales_to_embed)?; return Ok(()); } @@ -183,31 +172,17 @@ fn embed_all_utility_locales( // Embed locale files for each utility for util_name in &util_dirs { - let locale_path = src_uu_dir.join(util_name).join("locales/en-US.ftl"); - if locale_path.exists() { - let content = fs::read_to_string(&locale_path)?; - writeln!(embedded_file, " // Locale for {util_name}")?; - writeln!( - embedded_file, - " \"{util_name}/en-US.ftl\" => Some(r###\"{content}\"###)," - )?; - - // Tell Cargo to rerun if this file changes - println!("cargo:rerun-if-changed={}", locale_path.display()); - } + embed_component_locales(embedded_file, locales_to_embed, util_name, |locale| { + src_uu_dir + .join(util_name) + .join(format!("locales/{locale}.ftl")) + })?; } // Also embed uucore locale file if it exists - let uucore_locale_path = project_root.join("src/uucore/locales/en-US.ftl"); - if uucore_locale_path.exists() { - let content = fs::read_to_string(&uucore_locale_path)?; - writeln!(embedded_file, " // Common uucore locale")?; - writeln!( - embedded_file, - " \"uucore/en-US.ftl\" => Some(r###\"{content}\"###)," - )?; - println!("cargo:rerun-if-changed={}", uucore_locale_path.display()); - } + embed_component_locales(embedded_file, locales_to_embed, "uucore", |locale| { + project_root.join(format!("src/uucore/locales/{locale}.ftl")) + })?; embedded_file.flush()?; Ok(()) @@ -215,6 +190,7 @@ fn embed_all_utility_locales( fn embed_static_utility_locales( embedded_file: &mut std::fs::File, + locales_to_embed: &(String, Option), ) -> Result<(), Box> { use std::env; @@ -229,15 +205,9 @@ fn embed_static_utility_locales( }; // First, try to embed uucore locales - critical for common translations like "Usage:" - let uucore_locale_file = Path::new(&manifest_dir).join("locales/en-US.ftl"); - if uucore_locale_file.is_file() { - let content = std::fs::read_to_string(&uucore_locale_file)?; - writeln!(embedded_file, " // Common uucore locale")?; - writeln!( - embedded_file, - " \"uucore/en-US.ftl\" => Some(r###\"{content}\"###)," - )?; - } + embed_component_locales(embedded_file, locales_to_embed, "uucore", |locale| { + Path::new(&manifest_dir).join(format!("locales/{locale}.ftl")) + })?; // Collect and sort for deterministic builds let mut entries: Vec<_> = std::fs::read_dir(registry_dir)? @@ -251,15 +221,12 @@ fn embed_static_utility_locales( // Match uu_- if let Some((util_part, _)) = dir_name.split_once('-') { if let Some(util_name) = util_part.strip_prefix("uu_") { - let locale_file = entry.path().join("locales/en-US.ftl"); - if locale_file.is_file() { - let content = std::fs::read_to_string(&locale_file)?; - writeln!(embedded_file, " // Locale for {util_name}")?; - writeln!( - embedded_file, - " \"{util_name}/en-US.ftl\" => Some(r###\"{content}\"###)," - )?; - } + embed_component_locales( + embedded_file, + locales_to_embed, + util_name, + |locale| entry.path().join(format!("locales/{locale}.ftl")), + )?; } } } @@ -267,3 +234,251 @@ fn embed_static_utility_locales( Ok(()) } + +/// Determines which locales to embed into the binary. +/// +/// To support localized messages in installed binaries (e.g., via `cargo install`), +/// this function identifies the user's current locale from the `LANG` environment +/// variable. +/// +/// It always includes "en-US" to ensure that a fallback is available if the +/// system locale's translation file is missing or if `LANG` is not set. +fn get_locales_to_embed() -> (String, Option) { + let system_locale = env::var("LANG").ok().and_then(|lang| { + let locale = lang.split('.').next()?.replace('_', "-"); + if locale != "en-US" && !locale.is_empty() { + Some(locale) + } else { + None + } + }); + ("en-US".to_string(), system_locale) +} + +/// Helper function to iterate over the locales to embed. +fn for_each_locale( + locales: &(String, Option), + mut f: F, +) -> Result<(), Box> +where + F: FnMut(&str) -> Result<(), Box>, +{ + f(&locales.0)?; + if let Some(ref system_locale) = locales.1 { + f(system_locale)?; + } + Ok(()) +} + +/// Helper function to embed a single locale file. +fn embed_locale_file( + embedded_file: &mut std::fs::File, + locale_path: &Path, + locale_key: &str, + locale: &str, + component: &str, +) -> Result<(), Box> { + use std::fs; + + if locale_path.exists() || locale_path.is_file() { + let content = fs::read_to_string(locale_path)?; + writeln!( + embedded_file, + " // Locale for {component} ({locale})" + )?; + writeln!( + embedded_file, + " \"{locale_key}\" => Some(r###\"{content}\"###)," + )?; + + // Tell Cargo to rerun if this file changes + println!("cargo:rerun-if-changed={}", locale_path.display()); + } + Ok(()) +} + +/// Higher-level helper to embed locale files for a component with a path pattern. +/// This eliminates the repetitive for_each_locale + embed_locale_file pattern. +fn embed_component_locales( + embedded_file: &mut std::fs::File, + locales: &(String, Option), + component_name: &str, + path_builder: F, +) -> Result<(), Box> +where + F: Fn(&str) -> std::path::PathBuf, +{ + for_each_locale(locales, |locale| { + let locale_path = path_builder(locale); + embed_locale_file( + embedded_file, + &locale_path, + &format!("{component_name}/{locale}.ftl"), + locale, + component_name, + ) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn get_locales_to_embed_no_lang() { + unsafe { + env::remove_var("LANG"); + } + let (en_locale, system_locale) = get_locales_to_embed(); + assert_eq!(en_locale, "en-US"); + assert_eq!(system_locale, None); + + unsafe { + env::set_var("LANG", ""); + } + let (en_locale, system_locale) = get_locales_to_embed(); + assert_eq!(en_locale, "en-US"); + assert_eq!(system_locale, None); + unsafe { + env::remove_var("LANG"); + } + + unsafe { + env::set_var("LANG", "en_US.UTF-8"); + } + let (en_locale, system_locale) = get_locales_to_embed(); + assert_eq!(en_locale, "en-US"); + assert_eq!(system_locale, None); + unsafe { + env::remove_var("LANG"); + } + } + + #[test] + fn get_locales_to_embed_with_lang() { + unsafe { + env::set_var("LANG", "fr_FR.UTF-8"); + } + let (en_locale, system_locale) = get_locales_to_embed(); + assert_eq!(en_locale, "en-US"); + assert_eq!(system_locale, Some("fr-FR".to_string())); + unsafe { + env::remove_var("LANG"); + } + + unsafe { + env::set_var("LANG", "zh_CN.UTF-8"); + } + let (en_locale, system_locale) = get_locales_to_embed(); + assert_eq!(en_locale, "en-US"); + assert_eq!(system_locale, Some("zh-CN".to_string())); + unsafe { + env::remove_var("LANG"); + } + + unsafe { + env::set_var("LANG", "de"); + } + let (en_locale, system_locale) = get_locales_to_embed(); + assert_eq!(en_locale, "en-US"); + assert_eq!(system_locale, Some("de".to_string())); + unsafe { + env::remove_var("LANG"); + } + } + + #[test] + fn get_locales_to_embed_invalid_lang() { + // invalid locale format + unsafe { + env::set_var("LANG", "invalid"); + } + let (en_locale, system_locale) = get_locales_to_embed(); + assert_eq!(en_locale, "en-US"); + assert_eq!(system_locale, Some("invalid".to_string())); + unsafe { + env::remove_var("LANG"); + } + + // numeric values + unsafe { + env::set_var("LANG", "123"); + } + let (en_locale, system_locale) = get_locales_to_embed(); + assert_eq!(en_locale, "en-US"); + assert_eq!(system_locale, Some("123".to_string())); + unsafe { + env::remove_var("LANG"); + } + + // special characters + unsafe { + env::set_var("LANG", "@@@@"); + } + let (en_locale, system_locale) = get_locales_to_embed(); + assert_eq!(en_locale, "en-US"); + assert_eq!(system_locale, Some("@@@@".to_string())); + unsafe { + env::remove_var("LANG"); + } + + // malformed locale (no country code but with encoding) + unsafe { + env::set_var("LANG", "en.UTF-8"); + } + let (en_locale, system_locale) = get_locales_to_embed(); + assert_eq!(en_locale, "en-US"); + assert_eq!(system_locale, Some("en".to_string())); + unsafe { + env::remove_var("LANG"); + } + + // valid format but unusual locale + unsafe { + env::set_var("LANG", "XX_YY.UTF-8"); + } + let (en_locale, system_locale) = get_locales_to_embed(); + assert_eq!(en_locale, "en-US"); + assert_eq!(system_locale, Some("XX-YY".to_string())); + unsafe { + env::remove_var("LANG"); + } + } + + #[test] + fn for_each_locale_basic() { + let locales = ("en-US".to_string(), Some("fr-FR".to_string())); + let mut collected = Vec::new(); + + for_each_locale(&locales, |locale| { + collected.push(locale.to_string()); + Ok(()) + }) + .unwrap(); + + assert_eq!(collected, vec!["en-US", "fr-FR"]); + } + + #[test] + fn for_each_locale_no_system_locale() { + let locales = ("en-US".to_string(), None); + let mut collected = Vec::new(); + + for_each_locale(&locales, |locale| { + collected.push(locale.to_string()); + Ok(()) + }) + .unwrap(); + + assert_eq!(collected, vec!["en-US"]); + } + + #[test] + fn for_each_locale_error_handling() { + let locales = ("en-US".to_string(), Some("fr-FR".to_string())); + + let result = for_each_locale(&locales, |_locale| Err("test error".into())); + + assert!(result.is_err()); + } +} From 1eafb40628bb95bc1dac1763cd03906a34556ca6 Mon Sep 17 00:00:00 2001 From: mattsu <35655889+mattsu2020@users.noreply.github.com> Date: Tue, 11 Nov 2025 04:48:28 +0900 Subject: [PATCH 011/182] Improve fold bench data generation (#9210) --- src/uu/fold/benches/fold_bench.rs | 45 ++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/src/uu/fold/benches/fold_bench.rs b/src/uu/fold/benches/fold_bench.rs index abd69525f..d76ddbeaf 100644 --- a/src/uu/fold/benches/fold_bench.rs +++ b/src/uu/fold/benches/fold_bench.rs @@ -4,7 +4,6 @@ // file that was distributed with this source code. use divan::{Bencher, black_box}; -use std::fmt::Write; use uu_fold::uumain; use uucore::benchmark::{create_test_file, run_util_function}; @@ -12,12 +11,12 @@ use uucore::benchmark::{create_test_file, run_util_function}; #[divan::bench(args = [100_000])] fn fold_many_lines(bencher: Bencher, num_lines: usize) { let temp_dir = tempfile::tempdir().unwrap(); - // Create long lines that need folding - let data = (0..num_lines) - .fold(String::new(), |mut acc, i| { - writeln!(&mut acc, "This is a very long line number {i} that definitely needs to be folded at the default width of 80 columns").unwrap(); - acc - }); + let mut data = String::with_capacity(num_lines * 110); + for i in 0..num_lines { + data.push_str("This is a very long line number "); + append_usize(&mut data, i); + data.push_str(" that definitely needs to be folded at the default width of 80 columns\n"); + } let file_path = create_test_file(data.as_bytes(), temp_dir.path()); let file_path_str = file_path.to_str().unwrap(); @@ -30,14 +29,12 @@ fn fold_many_lines(bencher: Bencher, num_lines: usize) { #[divan::bench(args = [50_000])] fn fold_custom_width(bencher: Bencher, num_lines: usize) { let temp_dir = tempfile::tempdir().unwrap(); - let data = (0..num_lines).fold(String::new(), |mut acc, i| { - writeln!( - &mut acc, - "Line {i} with enough text to exceed width 40 characters and require folding" - ) - .unwrap(); - acc - }); + let mut data = String::with_capacity(num_lines * 80); + for i in 0..num_lines { + data.push_str("Line "); + append_usize(&mut data, i); + data.push_str(" with enough text to exceed width 40 characters and require folding\n"); + } let file_path = create_test_file(data.as_bytes(), temp_dir.path()); let file_path_str = file_path.to_str().unwrap(); @@ -49,3 +46,21 @@ fn fold_custom_width(bencher: Bencher, num_lines: usize) { fn main() { divan::main(); } + +fn append_usize(buf: &mut String, mut value: usize) { + let mut digits = [0u8; 20]; + let mut idx = digits.len(); + + if value == 0 { + buf.push('0'); + return; + } + + while value > 0 { + idx -= 1; + digits[idx] = b'0' + (value % 10) as u8; + value /= 10; + } + + buf.push_str(std::str::from_utf8(&digits[idx..]).unwrap()); +} From 997d9562533b7ed6a4949d419a7ce55fc4b461f9 Mon Sep 17 00:00:00 2001 From: E <79379754+oech3@users.noreply.github.com> Date: Tue, 11 Nov 2025 04:48:59 +0900 Subject: [PATCH 012/182] GNUmakefile: use PROFILE_CMD at make test (#9214) --- GNUmakefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GNUmakefile b/GNUmakefile index 01b4fd08c..0ccd0f68c 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -349,7 +349,7 @@ build: build-coreutils build-pkgs locales $(foreach test,$(UTILS),$(eval $(call TEST_BUSYBOX,$(test)))) test: - ${CARGO} test ${CARGOFLAGS} --features "$(TESTS) $(TEST_SPEC_FEATURE)" --no-default-features $(TEST_NO_FAIL_FAST) + ${CARGO} test ${CARGOFLAGS} --features "$(TESTS) $(TEST_SPEC_FEATURE)" $(PROFILE_CMD) --no-default-features $(TEST_NO_FAIL_FAST) nextest: ${CARGO} nextest run ${CARGOFLAGS} --features "$(TESTS) $(TEST_SPEC_FEATURE)" --no-default-features $(TEST_NO_FAIL_FAST) From db4439fc69be484eec01097540e47fc2dc72c768 Mon Sep 17 00:00:00 2001 From: E <79379754+oech3@users.noreply.github.com> Date: Tue, 11 Nov 2025 06:02:27 +0900 Subject: [PATCH 013/182] build-gnu.sh: Let SELinux optional to use it locally without libselinux --- .github/workflows/GnuTests.yml | 2 +- util/build-gnu.sh | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/.github/workflows/GnuTests.yml b/.github/workflows/GnuTests.yml index f82bbcabd..93716281f 100644 --- a/.github/workflows/GnuTests.yml +++ b/.github/workflows/GnuTests.yml @@ -244,7 +244,7 @@ jobs: ### Build - name: Build binaries run: | - lima bash -c "cd ~/work/uutils/ && bash util/build-gnu.sh --release-build" + lima bash -c "cd ~/work/uutils/ && SELINUX_ENABLED=1 bash util/build-gnu.sh --release-build" ### Run tests as user - name: Generate SELinux tests list diff --git a/util/build-gnu.sh b/util/build-gnu.sh index 99b921e69..75dfdb035 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -105,12 +105,8 @@ echo "UU_BUILD_DIR='${UU_BUILD_DIR}'" cd "${path_UUTILS}" && echo "[ pwd:'${PWD}' ]" -# Check for SELinux support -if [ "$(uname)" == "Linux" ]; then - # Only attempt to enable SELinux features on Linux - export SELINUX_ENABLED=1 - CARGO_FEATURE_FLAGS="${CARGO_FEATURE_FLAGS} selinux" -fi +export SELINUX_ENABLED # Run this script with=1 for testing SELinux +[ "${SELINUX_ENABLED}" = 1 ] && CARGO_FEATURE_FLAGS="${CARGO_FEATURE_FLAGS} selinux" # Trim leading whitespace from feature flags CARGO_FEATURE_FLAGS="$(echo "${CARGO_FEATURE_FLAGS}" | sed -e 's/^[[:space:]]*//')" From e3689d6e2f94643b39dd946e6ca5791edba9b9a1 Mon Sep 17 00:00:00 2001 From: E <79379754+oech3@users.noreply.github.com> Date: Tue, 11 Nov 2025 06:27:44 +0900 Subject: [PATCH 014/182] GNUmakefile: generalize logic for SELINUX_PROGS for other OS --- GNUmakefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GNUmakefile b/GNUmakefile index 0ccd0f68c..e277000a2 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -208,8 +208,8 @@ HASHSUM_PROGS := \ $(info Detected OS = $(OS)) -# Don't build the SELinux programs on macOS (Darwin) and FreeBSD -ifeq ($(filter $(OS),Darwin FreeBSD),$(OS)) +# Build the SELinux programs only on Linux +ifeq ($(filter $(OS),Linux),) SELINUX_PROGS := endif From b1180a28651e31c4c8f3d4d5bbb8c8856430f2b4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 11 Nov 2025 07:32:44 +0000 Subject: [PATCH 015/182] chore(deps): update rust crate crc-fast to v1.7.1 --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 12f788c83..26dea3931 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -744,9 +744,9 @@ checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "crc-fast" -version = "1.7.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "311eddc0ebdb918fb3f9ce10304736a8e94bfbe48e3dfd61c04754fdbb5a4d67" +checksum = "ffde0dda52b6befc15f7d1c573d2935cda15dc81bd546ef76d8679b2bf85a300" dependencies = [ "crc", "digest", From 9041489b66dd1f70d549b8466bcf525c22d9c3a3 Mon Sep 17 00:00:00 2001 From: E <79379754+oech3@users.noreply.github.com> Date: Tue, 11 Nov 2025 18:44:09 +0900 Subject: [PATCH 016/182] freebsd.yml: remove not working PROFILE= --- .github/workflows/freebsd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/freebsd.yml b/.github/workflows/freebsd.yml index c7a788a77..e75523b50 100644 --- a/.github/workflows/freebsd.yml +++ b/.github/workflows/freebsd.yml @@ -204,7 +204,7 @@ jobs: cargo nextest run --hide-progress-bar --profile ci --features "\$UUCORE_FEATURES" -p uucore || FAULT=1 fi # Test building with make - if (test -z "\$FAULT"); then make PROFILE=ci || FAULT=1 ; fi + if (test -z "\$FAULT"); then make || FAULT=1 ; fi # Clean to avoid to rsync back the files cargo clean if (test -n "\$FAULT"); then exit 1 ; fi From 7a35823051cb2139c8f18f012499d58062c9756a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 11 Nov 2025 10:56:42 +0000 Subject: [PATCH 017/182] chore(deps): update rust crate indicatif to v0.18.3 --- Cargo.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 26dea3931..8c9f0c735 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1563,9 +1563,9 @@ dependencies = [ [[package]] name = "indicatif" -version = "0.18.2" +version = "0.18.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ade6dfcba0dfb62ad59e59e7241ec8912af34fd29e0e743e3db992bd278e8b65" +checksum = "9375e112e4b463ec1b1c6c011953545c65a30164fbab5b581df32b3abf0dcb88" dependencies = [ "console", "portable-atomic", @@ -1751,7 +1751,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", - "windows-targets 0.53.2", + "windows-targets 0.52.6", ] [[package]] From 61938133ec42e4a896eda3d660928b038845de9d Mon Sep 17 00:00:00 2001 From: E <79379754+oech3@users.noreply.github.com> Date: Tue, 11 Nov 2025 20:57:15 +0900 Subject: [PATCH 018/182] GNUmakefile: Remove check for LIBSELINUX_ENABLED --- GNUmakefile | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/GNUmakefile b/GNUmakefile index e277000a2..01cf14437 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -75,19 +75,6 @@ ifeq ($(OS),Windows_NT) endif LN ?= ln -sf -ifdef SELINUX_ENABLED - override SELINUX_ENABLED := 0 -# Now check if we should enable it (only on non-Windows) - ifneq ($(OS),Windows_NT) - ifeq ($(shell if [ -x /sbin/selinuxenabled ] && /sbin/selinuxenabled 2>/dev/null; then echo 0; else echo 1; fi),0) - override SELINUX_ENABLED := 1 -$(info /sbin/selinuxenabled successful) - else -$(info SELINUX_ENABLED=1 but /sbin/selinuxenabled failed) - endif - endif -endif - # Possible programs PROGS := \ arch \ From f264a40aa8ea5fa3fa8bd030aff0ce718c811cc1 Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Tue, 11 Nov 2025 17:36:17 +0100 Subject: [PATCH 019/182] readlink: test calling without args --- tests/by-util/test_readlink.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/by-util/test_readlink.rs b/tests/by-util/test_readlink.rs index 848800cc3..e21459526 100644 --- a/tests/by-util/test_readlink.rs +++ b/tests/by-util/test_readlink.rs @@ -2,11 +2,11 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +// // spell-checker:ignore regfile -use uutests::new_ucmd; -use uutests::path_concat; + use uutests::util::{TestScenario, get_root_path}; -use uutests::{at_and_ucmd, util_name}; +use uutests::{at_and_ucmd, new_ucmd, path_concat, util_name}; static GIBBERISH: &str = "supercalifragilisticexpialidocious"; @@ -15,6 +15,14 @@ static NOT_A_DIRECTORY: &str = "Not a directory"; #[cfg(windows)] static NOT_A_DIRECTORY: &str = "The directory name is invalid."; +#[test] +fn test_no_args() { + new_ucmd!() + .fails_with_code(1) + .no_stdout() + .stderr_contains("readlink: missing operand"); +} + #[test] fn test_invalid_arg() { new_ucmd!().arg("--definitely-invalid").fails_with_code(1); From 20752002172e32ac479a5654a759c9161c792865 Mon Sep 17 00:00:00 2001 From: E <79379754+oech3@users.noreply.github.com> Date: Wed, 12 Nov 2025 09:35:41 +0900 Subject: [PATCH 020/182] GNUmakefile: drop not used use_default:=1 --- GNUmakefile | 2 -- 1 file changed, 2 deletions(-) diff --git a/GNUmakefile b/GNUmakefile index 01cf14437..b13dbbd34 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -317,8 +317,6 @@ endif all: build -use_default := 1 - build-pkgs: ifneq (${MULTICALL}, y) ifdef BUILD_SPEC_FEATURE From 2930c93706060b2234f1486ab2b2b76223f7124d Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Tue, 11 Nov 2025 20:43:55 -0700 Subject: [PATCH 021/182] Update redoxer and reenable Redox OS in CI --- .github/workflows/CICD.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index e0a4a9141..f5ec1cfbc 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -582,8 +582,7 @@ jobs: - { os: ubuntu-latest , target: x86_64-unknown-linux-gnu , features: "feat_os_unix,test_risky_names", use-cross: use-cross } - { 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 } - # broken by network error - # - { os: ubuntu-latest , target: x86_64-unknown-redox , features: feat_os_unix_redox , use-cross: redoxer , skip-tests: true } + - { os: ubuntu-latest , target: x86_64-unknown-redox , features: feat_os_unix_redox , use-cross: redoxer , skip-tests: true } - { os: ubuntu-latest , target: wasm32-unknown-unknown , default-features: false, features: uucore/format, skip-tests: true, skip-package: true, skip-publish: true } - { os: macos-latest , target: aarch64-apple-darwin , features: feat_os_macos, workspace-tests: true } # M1 CPU # PR #7964: Mac should still build even if the feature is not enabled @@ -783,7 +782,7 @@ jobs: - uses: taiki-e/install-action@v2 if: steps.vars.outputs.CARGO_CMD == 'redoxer' with: - tool: redoxer@0.2.37 + tool: redoxer@0.2.56 - name: Initialize toolchain-dependent workflow variables id: dep_vars shell: bash From 319c1e2dfa0e0b095881b218c78ed08793ba5429 Mon Sep 17 00:00:00 2001 From: E <79379754+oech3@users.noreply.github.com> Date: Wed, 12 Nov 2025 13:37:47 +0900 Subject: [PATCH 022/182] ci: Mark runcon-no-reorder as SELinux required --- util/gnu-patches/runcon-no-reorder.patch | 14 ++++++++++++++ util/gnu-patches/series | 1 + 2 files changed, 15 insertions(+) create mode 100644 util/gnu-patches/runcon-no-reorder.patch diff --git a/util/gnu-patches/runcon-no-reorder.patch b/util/gnu-patches/runcon-no-reorder.patch new file mode 100644 index 000000000..833e37dca --- /dev/null +++ b/util/gnu-patches/runcon-no-reorder.patch @@ -0,0 +1,14 @@ +--git a/tests/runcon/runcon-no-reorder.sh b/tests/runcon/runcon-no-reorder.sh +index 2027555..956c51e 100644 +--- a/tests/runcon/runcon-no-reorder.sh ++++ b/tests/runcon/runcon-no-reorder.sh +@@ -16,6 +16,9 @@ + # You should have received a copy of the GNU General Public License + # along with this program. If not, see . + ++# We don't have runcon buildable without libselinux. ++_require_selinux_ ++ + . "${srcdir=.}/tests/init.sh"; path_prepend_ ./src + print_ver_ runcon + diff --git a/util/gnu-patches/series b/util/gnu-patches/series index 5fb1398cd..e47d52242 100644 --- a/util/gnu-patches/series +++ b/util/gnu-patches/series @@ -11,3 +11,4 @@ tests_tsort.patch tests_du_move_dir_while_traversing.patch test_mkdir_restorecon.patch error_msg_uniq.diff +runcon-no-reorder.patch From ed03c1d103a6bf9ca64bad4fed02a56a0dc93a7f Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Wed, 12 Nov 2025 17:25:06 +0900 Subject: [PATCH 023/182] openbsd.yml: Remove not working PROFILE= --- .github/workflows/openbsd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/openbsd.yml b/.github/workflows/openbsd.yml index 9dad6ebc8..1c04528c0 100644 --- a/.github/workflows/openbsd.yml +++ b/.github/workflows/openbsd.yml @@ -188,7 +188,7 @@ jobs: cargo test --features "\$UUCORE_FEATURES" -p uucore || FAULT=1 fi # Test building with make - if (test -z "\$FAULT"); then make PROFILE=ci || FAULT=1 ; fi + if (test -z "\$FAULT"); then make || FAULT=1 ; fi # Clean to avoid to rsync back the files cargo clean if (test -n "\$FAULT"); then exit 1 ; fi From abcfbe407e5b284d8fe8ecf3393a67d2c507293e Mon Sep 17 00:00:00 2001 From: naoNao89 <90588855+naoNao89@users.noreply.github.com> Date: Sun, 9 Nov 2025 04:52:46 +0700 Subject: [PATCH 024/182] fix(nl): allow repeated flags to match GNU behavior MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Enable args_override_self(true) for repeated flags • Remove unnecessary ArgAction::Append calls • Fix tests to verify last value wins, not just succeeds • Update test output validation for repeated flags Fixes #9132 --- src/uu/nl/src/nl.rs | 1 + tests/by-util/test_nl.rs | 101 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+) diff --git a/src/uu/nl/src/nl.rs b/src/uu/nl/src/nl.rs index e94fa847d..7d1f862aa 100644 --- a/src/uu/nl/src/nl.rs +++ b/src/uu/nl/src/nl.rs @@ -245,6 +245,7 @@ pub fn uu_app() -> Command { .after_help(translate!("nl-after-help")) .infer_long_args(true) .disable_help_flag(true) + .args_override_self(true) .arg( Arg::new(options::HELP) .long(options::HELP) diff --git a/tests/by-util/test_nl.rs b/tests/by-util/test_nl.rs index 953d7bcb3..ab430b20b 100644 --- a/tests/by-util/test_nl.rs +++ b/tests/by-util/test_nl.rs @@ -800,3 +800,104 @@ fn test_file_with_non_utf8_content() { String::from_utf8_lossy(invalid_utf8) )); } + +// Regression tests for issue #9132: repeated flags should use last value +#[test] +fn test_repeated_body_numbering_flag() { + // -ba -bt should use -bt (t=nonempty) + new_ucmd!() + .args(&["-ba", "-bt"]) + .pipe_in("a\n\nb\n\nc") + .succeeds() + .stdout_is(" 1\ta\n \n 2\tb\n \n 3\tc\n"); +} + +#[test] +fn test_repeated_header_numbering_flag() { + // -ha -ht should use -ht (number only nonempty lines in header) + new_ucmd!() + .args(&["-ha", "-ht"]) + .pipe_in("\\:\\:\\:\na\nb\n\nc") + .succeeds() + .stdout_is("\n 1\ta\n 2\tb\n \n 3\tc\n"); +} + +#[test] +fn test_repeated_footer_numbering_flag() { + // -fa -ft should use -ft (t=nonempty in footer) + new_ucmd!() + .args(&["-fa", "-ft"]) + .pipe_in("\\:\na\nb\n\nc") + .succeeds() + .stdout_is("\n 1\ta\n 2\tb\n \n 3\tc\n"); +} + +#[test] +fn test_repeated_number_format_flag() { + // -n ln -n rn should use -n rn (rn=right aligned) + new_ucmd!() + .args(&["-n", "ln", "-n", "rn"]) + .pipe_in("a\nb\nc") + .succeeds() + .stdout_is(" 1\ta\n 2\tb\n 3\tc\n"); +} + +#[test] +fn test_repeated_number_separator_flag() { + // -s ':' -s '|' should use -s '|' + new_ucmd!() + .args(&["-s", ":", "-s", "|"]) + .pipe_in("a\nb\nc") + .succeeds() + .stdout_is(" 1|a\n 2|b\n 3|c\n"); +} + +#[test] +fn test_repeated_number_width_flag() { + // -w 3 -w 8 should use -w 8 + new_ucmd!() + .args(&["-w", "3", "-w", "8"]) + .pipe_in("a\nb\nc") + .succeeds() + .stdout_is(" 1\ta\n 2\tb\n 3\tc\n"); +} + +#[test] +fn test_repeated_line_increment_flag() { + // -i 1 -i 5 should use -i 5 + new_ucmd!() + .args(&["-i", "1", "-i", "5"]) + .pipe_in("a\nb\nc") + .succeeds() + .stdout_is(" 1\ta\n 6\tb\n 11\tc\n"); +} + +#[test] +fn test_repeated_starting_line_number_flag() { + // -v 1 -v 10 should use -v 10 + new_ucmd!() + .args(&["-v", "1", "-v", "10"]) + .pipe_in("a\nb\nc") + .succeeds() + .stdout_is(" 10\ta\n 11\tb\n 12\tc\n"); +} + +#[test] +fn test_repeated_join_blank_lines_flag() { + // -l 1 -l 2 should use -l 2 + new_ucmd!() + .args(&["-l", "1", "-l", "2", "-ba"]) + .pipe_in("a\n\n\nb") + .succeeds() + .stdout_is(" 1\ta\n \n 2\t\n 3\tb\n"); +} + +#[test] +fn test_repeated_section_delimiter_flag() { + // -d ':' -d '|' should use -d '|' + new_ucmd!() + .args(&["-d", ":", "-d", "|"]) + .pipe_in("|:|:|:\na\nb\nc") + .succeeds() + .stdout_is("\n a\n b\n c\n"); +} From 96c63f174bc6679c53bbf867e501fe70eb693a8f Mon Sep 17 00:00:00 2001 From: Laurent Cheylus Date: Wed, 12 Nov 2025 15:36:22 +0100 Subject: [PATCH 025/182] OpenBSD CI: increase max open files for test job Signed-off-by: Laurent Cheylus --- .github/workflows/openbsd.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/openbsd.yml b/.github/workflows/openbsd.yml index 1c04528c0..26012e017 100644 --- a/.github/workflows/openbsd.yml +++ b/.github/workflows/openbsd.yml @@ -152,6 +152,8 @@ jobs: sudo -i -u ${TEST_USER} sh << EOF set -e whoami + # Increase max open files (512 by default) + ulimit -n 1024 # Rust is installed from packages, no need for rustup # Set up PATH for cargo export PATH="/usr/local/bin:$PATH" From d1cf2ffd36908cc6b2d208b7353cdaa411cfbd22 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Wed, 12 Nov 2025 19:28:42 +0900 Subject: [PATCH 026/182] Cargo.toml: move panic=abort to release profile (save binary size) --- Cargo.toml | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 499eb8741..07b578021 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -587,17 +587,13 @@ name = "uudoc" path = "src/bin/uudoc.rs" required-features = ["uudoc"] -# The default release profile. It contains all optimizations. -# With this profile (like in the standard release profile), -# the stack traces will still be available. +# The default release profile with some optimizations. [profile.release] lto = true +panic = "abort" -# A release-like profile that is tuned to be fast, even when being fast -# compromises on binary size. This includes aborting on panic. [profile.release-fast] inherits = "release" -panic = "abort" codegen-units = 1 # A release-like profile that is as small as possible. @@ -606,10 +602,11 @@ inherits = "release-fast" opt-level = "z" strip = true -# A release-like profile with debug info, useful for profiling. +# A release-like profile with debug info for profiling. # See https://github.com/mstange/samply . [profile.profiling] inherits = "release" +panic = "unwind" debug = true [lints] From d17e89bdfe093ff0b9b1050c1585a00e83b3c2a9 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Thu, 13 Nov 2025 05:03:02 +0900 Subject: [PATCH 027/182] build-gnu.sh: Use system's GNU tools --- util/build-gnu.sh | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/util/build-gnu.sh b/util/build-gnu.sh index 75dfdb035..bd14348b1 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -8,21 +8,11 @@ set -e -# Use GNU version for make, nproc, readlink and sed on *BSD -case "$OSTYPE" in - *bsd*) - MAKE="gmake" - NPROC="gnproc" - READLINK="greadlink" - SED="gsed" - ;; - *) - MAKE="make" - NPROC="nproc" - READLINK="readlink" - SED="sed" - ;; -esac +# Use system's GNU version for make, nproc, readlink and sed on *BSD +MAKE=$(command -v gmake||command -v make) +NPROC=$(command -v gnproc||command -v nproc) +READLINK=$(command -v greadlink||command -v readlink) +SED=$(command -v gsed||command -v sed) ME="${0}" ME_dir="$(dirname -- "$("${READLINK}" -fm -- "${ME}")")" From 9887a3c9bb88fcc1c9fb2fc2f56c4c293a7f0c34 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Thu, 13 Nov 2025 06:13:43 +0900 Subject: [PATCH 028/182] build-gnu.sh: Cleanup logic for system bins --- util/build-gnu.sh | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/util/build-gnu.sh b/util/build-gnu.sh index bd14348b1..e1388570a 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -39,24 +39,8 @@ path_GNU="$("${READLINK}" -fm -- "${path_GNU:-${path_UUTILS}/../gnu}")" ### -# On MacOS there is no system /usr/bin/timeout -# and trying to add it to /usr/bin (with symlink of copy binary) will fail unless system integrity protection is disabled (not ideal) -# ref: https://support.apple.com/en-us/102149 -# On MacOS the Homebrew coreutils could be installed and then "sudo ln -s /opt/homebrew/bin/timeout /usr/local/bin/timeout" -# Set to /usr/local/bin/timeout instead if /usr/bin/timeout is not found -SYSTEM_TIMEOUT="timeout" -if [ -x /usr/bin/timeout ]; then - SYSTEM_TIMEOUT="/usr/bin/timeout" -elif [ -x /usr/local/bin/timeout ]; then - SYSTEM_TIMEOUT="/usr/local/bin/timeout" -fi - -SYSTEM_YES="yes" -if [ -x /usr/bin/yes ]; then - SYSTEM_YES="/usr/bin/yes" -elif [ -x /usr/local/bin/yes ]; then - SYSTEM_YES="/usr/local/bin/yes" -fi +SYSTEM_TIMEOUT=$(command -v timeout) +SYSTEM_YES=$(command -v yes) ### From 91930760ae285a26acf86bf33cabd74b1f9501af Mon Sep 17 00:00:00 2001 From: naoNao89 <90588855+naoNao89@users.noreply.github.com> Date: Thu, 13 Nov 2025 02:27:58 +0700 Subject: [PATCH 029/182] benches: Port factor benchmarks from Criterion to Divan Replace Criterion with Divan to align with all other benchmarks in the codebase (22 packages use Divan, only factor used Criterion). Eliminates the html_reports warning and consolidates on a single benchmarking framework across the project. --- Cargo.lock | 149 ++------------------------ tests/benches/factor/Cargo.toml | 2 +- tests/benches/factor/benches/table.rs | 34 +++--- 3 files changed, 24 insertions(+), 161 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8c9f0c735..10c1e72c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,12 +32,6 @@ dependencies = [ "libc", ] -[[package]] -name = "anes" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" - [[package]] name = "ansi-width" version = "0.1.0" @@ -303,12 +297,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" -[[package]] -name = "cast" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" - [[package]] name = "cc" version = "1.2.27" @@ -350,33 +338,6 @@ dependencies = [ "windows-link 0.2.1", ] -[[package]] -name = "ciborium" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" -dependencies = [ - "ciborium-io", - "ciborium-ll", - "serde", -] - -[[package]] -name = "ciborium-io" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" - -[[package]] -name = "ciborium-ll" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" -dependencies = [ - "ciborium-io", - "half", -] - [[package]] name = "clang-sys" version = "1.8.1" @@ -762,39 +723,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "criterion" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bf7af66b0989381bd0be551bd7cc91912a655a58c6918420c9527b1fd8b4679" -dependencies = [ - "anes", - "cast", - "ciborium", - "clap", - "criterion-plot", - "itertools 0.13.0", - "num-traits", - "oorandom", - "plotters", - "rayon", - "regex", - "serde", - "serde_json", - "tinytemplate", - "walkdir", -] - -[[package]] -name = "criterion-plot" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" -dependencies = [ - "cast", - "itertools 0.10.5", -] - [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -1619,15 +1547,6 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" -[[package]] -name = "itertools" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.13.0" @@ -1664,7 +1583,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde_core", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1751,7 +1670,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", - "windows-targets 0.52.6", + "windows-targets 0.53.2", ] [[package]] @@ -1962,7 +1881,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]] @@ -2076,12 +1995,6 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "oorandom" -version = "11.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" - [[package]] name = "ordered-multimap" version = "0.7.3" @@ -2208,34 +2121,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "plotters" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" -dependencies = [ - "num-traits", - "plotters-backend", - "plotters-svg", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "plotters-backend" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" - -[[package]] -name = "plotters-svg" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" -dependencies = [ - "plotters-backend", -] - [[package]] name = "portable-atomic" version = "1.11.1" @@ -2567,7 +2452,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.11.0", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2867,7 +2752,7 @@ dependencies = [ "getrandom 0.3.3", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2984,16 +2869,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "tinytemplate" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" -dependencies = [ - "serde", - "serde_json", -] - [[package]] name = "toml_datetime" version = "0.6.11" @@ -3461,7 +3336,7 @@ name = "uu_factor_benches" version = "0.0.0" dependencies = [ "array-init", - "criterion", + "codspeed-divan-compat", "num-prime", "rand 0.9.2", "rand_chacha 0.9.0", @@ -4505,16 +4380,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "web-sys" -version = "0.3.77" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - [[package]] name = "web-time" version = "1.1.0" @@ -4556,7 +4421,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/tests/benches/factor/Cargo.toml b/tests/benches/factor/Cargo.toml index 30d4cc8b1..02e59925c 100644 --- a/tests/benches/factor/Cargo.toml +++ b/tests/benches/factor/Cargo.toml @@ -10,7 +10,7 @@ publish = false [dev-dependencies] array-init = "2.0.0" -criterion = "0.6.0" +divan = { workspace = true } rand = "0.9.1" rand_chacha = "0.9.0" num-prime = "0.4.4" diff --git a/tests/benches/factor/benches/table.rs b/tests/benches/factor/benches/table.rs index 504b7e8e3..4d89282fa 100644 --- a/tests/benches/factor/benches/table.rs +++ b/tests/benches/factor/benches/table.rs @@ -6,9 +6,14 @@ // spell-checker:ignore funcs use array_init::array_init; -use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main}; +use divan::Bencher; -fn table(c: &mut Criterion) { +fn main() { + divan::main(); +} + +#[divan::bench()] +fn factor_table(bencher: Bencher) { #[cfg(target_os = "linux")] check_personality(); @@ -22,21 +27,17 @@ fn table(c: &mut Criterion) { let mut rng = ChaCha8Rng::seed_from_u64(SEED); std::iter::repeat_with(move || array_init::<_, _, INPUT_SIZE>(|_| rng.next_u64())) + .take(10) + .collect::>() }; - let mut group = c.benchmark_group("table"); - group.throughput(Throughput::Elements(INPUT_SIZE as _)); - for a in inputs.take(10) { - let a_str = format!("{a:?}"); - group.bench_with_input(BenchmarkId::new("factor", &a_str), &a, |b, &a| { - b.iter(|| { - for n in a { - let _r = num_prime::nt_funcs::factors(n, None); - } - }); - }); - } - group.finish(); + bencher.bench(|| { + for a in &inputs { + for n in a { + divan::black_box(num_prime::nt_funcs::factors(*n, None)); + } + } + }); } #[cfg(target_os = "linux")] @@ -59,6 +60,3 @@ fn check_personality() { ); } } - -criterion_group!(benches, table); -criterion_main!(benches); From 4d71a6e2d2145655e24d94f46fb48e02160992db Mon Sep 17 00:00:00 2001 From: Adrian Kretz Date: Thu, 13 Nov 2025 00:13:24 +0100 Subject: [PATCH 030/182] test/install: add test to ignore umask --- tests/by-util/test_install.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/by-util/test_install.rs b/tests/by-util/test_install.rs index f4c7b2dc1..1e78b28da 100644 --- a/tests/by-util/test_install.rs +++ b/tests/by-util/test_install.rs @@ -243,6 +243,29 @@ fn test_install_mode_symbolic() { assert_eq!(0o100_003_u32, PermissionsExt::mode(&permissions)); } +#[test] +fn test_install_mode_symbolic_ignore_umask() { + let (at, mut ucmd) = at_and_ucmd!(); + let dir = "target_dir"; + let file = "source_file"; + let mode_arg = "--mode=+w"; + + at.touch(file); + at.mkdir(dir); + ucmd.arg(file) + .arg(dir) + .arg(mode_arg) + .umask(0o022) + .succeeds() + .no_stderr(); + + let dest_file = &format!("{dir}/{file}"); + assert!(at.file_exists(file)); + assert!(at.file_exists(dest_file)); + let permissions = at.metadata(dest_file).permissions(); + assert_eq!(0o100_222_u32, PermissionsExt::mode(&permissions)); +} + #[test] fn test_install_mode_failing() { let (at, mut ucmd) = at_and_ucmd!(); From 376b88ac72dd5e63d2342721610b07d3dc2fadfc Mon Sep 17 00:00:00 2001 From: Adrian Kretz Date: Thu, 13 Nov 2025 00:14:00 +0100 Subject: [PATCH 031/182] install: ignore umask with symbolic mode --- src/uu/install/src/install.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/uu/install/src/install.rs b/src/uu/install/src/install.rs index 4242cc04b..49252dcf9 100644 --- a/src/uu/install/src/install.rs +++ b/src/uu/install/src/install.rs @@ -25,7 +25,6 @@ use uucore::display::Quotable; use uucore::entries::{grp2gid, usr2uid}; use uucore::error::{FromIo, UError, UResult, UUsageError}; use uucore::fs::dir_strip_dot_for_creation; -use uucore::mode::get_umask; use uucore::perms::{Verbosity, VerbosityLevel, wrap_chown}; use uucore::process::{getegid, geteuid}; #[cfg(feature = "selinux")] @@ -339,7 +338,7 @@ fn behavior(matches: &ArgMatches) -> UResult { let specified_mode: Option = if matches.contains_id(OPT_MODE) { let x = matches.get_one::(OPT_MODE).ok_or(1)?; - Some(mode::parse(x, considering_dir, get_umask()).map_err(|err| { + Some(mode::parse(x, considering_dir, 0).map_err(|err| { show_error!( "{}", translate!("install-error-invalid-mode", "error" => err) From bbfc1547932d5788558d5a85168cdeb2e885e8ce Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Thu, 13 Nov 2025 17:36:48 +0900 Subject: [PATCH 032/182] build-gnu.sh: Remove || true --- util/build-gnu.sh | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/util/build-gnu.sh b/util/build-gnu.sh index 75dfdb035..681ec5b07 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -132,10 +132,7 @@ cd - # Pass the feature flags to make, which will pass them to cargo "${MAKE}" PROFILE="${UU_MAKE_PROFILE}" CARGOFLAGS="${CARGO_FEATURE_FLAGS}" -touch g -echo "stat with selinux support" -./target/debug/stat -c%C g || true -rm g +[ ${SELINUX_ENABLED} = 1 ] && touch g && echo "stat with selinux support" && "${UU_MAKE_PROFILE}"/stat -c%C g && rm g cp "${UU_BUILD_DIR}/install" "${UU_BUILD_DIR}/ginstall" # The GNU tests rename this script before running, to avoid confusion with the make target # Create *sum binaries From e9ef08622d71c9405feff92cd93fe6f519210f2a Mon Sep 17 00:00:00 2001 From: Laurent Cheylus Date: Thu, 13 Nov 2025 09:40:11 +0100 Subject: [PATCH 033/182] hostname: enable test test_hostname_ip on OpenBSD Signed-off-by: Laurent Cheylus --- tests/by-util/test_hostname.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/by-util/test_hostname.rs b/tests/by-util/test_hostname.rs index aedf7d015..e4f30c3e5 100644 --- a/tests/by-util/test_hostname.rs +++ b/tests/by-util/test_hostname.rs @@ -14,8 +14,8 @@ fn test_hostname() { assert!(ls_default_res.stdout().len() >= ls_domain_res.stdout().len()); } -// FixME: fails for "MacOS" and "OpenBSD" => "failed to lookup address information" -#[cfg(not(any(target_os = "macos", target_os = "openbsd")))] +// FixME: fails for "MacOS" => "failed to lookup address information" +#[cfg(not(any(target_os = "macos")))] #[test] fn test_hostname_ip() { let result = new_ucmd!().arg("-i").succeeds(); From e1548c41b9cc5c72c27bf87ff92995d33b9cd9a9 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Thu, 13 Nov 2025 17:56:08 +0900 Subject: [PATCH 034/182] Remove an echo --- util/build-gnu.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/util/build-gnu.sh b/util/build-gnu.sh index 681ec5b07..30ad63650 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -132,7 +132,8 @@ cd - # Pass the feature flags to make, which will pass them to cargo "${MAKE}" PROFILE="${UU_MAKE_PROFILE}" CARGOFLAGS="${CARGO_FEATURE_FLAGS}" -[ ${SELINUX_ENABLED} = 1 ] && touch g && echo "stat with selinux support" && "${UU_MAKE_PROFILE}"/stat -c%C g && rm g +# min test for SELinux +[ ${SELINUX_ENABLED} = 1 ] && touch g && "${UU_MAKE_PROFILE}"/stat -c%C g && rm g cp "${UU_BUILD_DIR}/install" "${UU_BUILD_DIR}/ginstall" # The GNU tests rename this script before running, to avoid confusion with the make target # Create *sum binaries From 4ee3edf6ae9cb6b0619e2c3b9e664e03b4503405 Mon Sep 17 00:00:00 2001 From: Laurent Cheylus Date: Thu, 13 Nov 2025 09:40:11 +0100 Subject: [PATCH 035/182] OpenBSD workflow: add fake host for reverse DNS lookup in test job Signed-off-by: Laurent Cheylus --- .github/workflows/openbsd.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/openbsd.yml b/.github/workflows/openbsd.yml index 26012e017..9ca20eab3 100644 --- a/.github/workflows/openbsd.yml +++ b/.github/workflows/openbsd.yml @@ -147,6 +147,8 @@ jobs: useradd -m -G wheel ${TEST_USER} chown -R ${TEST_USER}:wheel /root/ "${WORKSPACE_PARENT}"/ whoami + # Add fake host for reverse DNS lookup (needed for hostname test) + printf "10.0.2.15\topenbsd.my.domain openbsd\n" >> /etc/hosts # # Further work needs to be done in a sudo as we are changing users sudo -i -u ${TEST_USER} sh << EOF From 44ae621296b0be76c15b425530b56838dec9f6cc Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Thu, 13 Nov 2025 06:51:07 +0900 Subject: [PATCH 036/182] show-utils.sh use GNU realpath --- util/show-utils.sh | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/util/show-utils.sh b/util/show-utils.sh index 0a41698d9..3cc487940 100755 --- a/util/show-utils.sh +++ b/util/show-utils.sh @@ -5,14 +5,7 @@ # spell-checker:ignore (jq) deps startswith # Use GNU version for realpath on *BSD -case "$OSTYPE" in - *bsd*) - REALPATH="grealpath" - ;; - *) - REALPATH="realpath" - ;; -esac +REALPATH=$(command -v grealpath||command -v realpath) ME="${0}" ME_dir="$(dirname -- "${ME}")" From 4a48c9e35ede5f70162dbe139d713cab95b89558 Mon Sep 17 00:00:00 2001 From: mattsu <35655889+mattsu2020@users.noreply.github.com> Date: Thu, 13 Nov 2025 19:41:39 +0900 Subject: [PATCH 037/182] Merge pull request #9126 from mattsu2020/fold_fix fix(fold): GNU fold-characters.sh test --- .../cspell.dictionaries/jargon.wordlist.txt | 1 + Cargo.lock | 1 + src/uu/fold/Cargo.toml | 1 + src/uu/fold/locales/en-US.ftl | 1 + src/uu/fold/locales/fr-FR.ftl | 1 + src/uu/fold/src/fold.rs | 422 +++++++++++++++--- tests/by-util/test_fold.rs | 18 + 7 files changed, 378 insertions(+), 67 deletions(-) diff --git a/.vscode/cspell.dictionaries/jargon.wordlist.txt b/.vscode/cspell.dictionaries/jargon.wordlist.txt index 70cbd9379..a3b51bfed 100644 --- a/.vscode/cspell.dictionaries/jargon.wordlist.txt +++ b/.vscode/cspell.dictionaries/jargon.wordlist.txt @@ -120,6 +120,7 @@ pseudoprimes quantiles readonly reparse +rposition seedable semver semiprime diff --git a/Cargo.lock b/Cargo.lock index 8c9f0c735..f2f8412cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3495,6 +3495,7 @@ dependencies = [ "codspeed-divan-compat", "fluent", "tempfile", + "unicode-width 0.2.2", "uucore", ] diff --git a/src/uu/fold/Cargo.toml b/src/uu/fold/Cargo.toml index 644d78b41..845ce2c96 100644 --- a/src/uu/fold/Cargo.toml +++ b/src/uu/fold/Cargo.toml @@ -21,6 +21,7 @@ path = "src/fold.rs" clap = { workspace = true } uucore = { workspace = true } fluent = { workspace = true } +unicode-width = { workspace = true } [dev-dependencies] divan = { workspace = true } diff --git a/src/uu/fold/locales/en-US.ftl b/src/uu/fold/locales/en-US.ftl index 9f8c6f3b9..d42416667 100644 --- a/src/uu/fold/locales/en-US.ftl +++ b/src/uu/fold/locales/en-US.ftl @@ -2,6 +2,7 @@ fold-about = Writes each file (or standard input if no files are given) to standard output whilst breaking long lines fold-usage = fold [OPTION]... [FILE]... fold-bytes-help = count using bytes rather than columns (meaning control characters such as newline are not treated specially) +fold-characters-help = count using character positions rather than display columns fold-spaces-help = break lines at word boundaries rather than a hard cut-off fold-width-help = set WIDTH as the maximum line width rather than 80 fold-error-illegal-width = illegal width value diff --git a/src/uu/fold/locales/fr-FR.ftl b/src/uu/fold/locales/fr-FR.ftl index 1a7235940..ce313160c 100644 --- a/src/uu/fold/locales/fr-FR.ftl +++ b/src/uu/fold/locales/fr-FR.ftl @@ -1,6 +1,7 @@ fold-about = Écrit chaque fichier (ou l'entrée standard si aucun fichier n'est donné) sur la sortie standard en coupant les lignes trop longues fold-usage = fold [OPTION]... [FICHIER]... fold-bytes-help = compter en octets plutôt qu'en colonnes (les caractères de contrôle comme retour chariot ne sont pas traités spécialement) +fold-characters-help = compter en caractères plutôt qu'en colonnes d'affichage fold-spaces-help = couper les lignes aux limites de mots plutôt qu'à une largeur fixe fold-width-help = définir WIDTH comme largeur de ligne maximale au lieu de 80 fold-error-illegal-width = valeur de largeur illégale diff --git a/src/uu/fold/src/fold.rs b/src/uu/fold/src/fold.rs index bbaac56be..f14ed3cf0 100644 --- a/src/uu/fold/src/fold.rs +++ b/src/uu/fold/src/fold.rs @@ -9,6 +9,7 @@ use clap::{Arg, ArgAction, Command}; use std::fs::File; use std::io::{BufRead, BufReader, BufWriter, Read, Write, stdin, stdout}; use std::path::Path; +use unicode_width::UnicodeWidthChar; use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError}; use uucore::format_usage; @@ -21,11 +22,28 @@ const TAB: u8 = b'\t'; mod options { pub const BYTES: &str = "bytes"; + pub const CHARACTERS: &str = "characters"; pub const SPACES: &str = "spaces"; pub const WIDTH: &str = "width"; pub const FILE: &str = "file"; } +#[derive(Clone, Copy, PartialEq, Eq)] +enum WidthMode { + Columns, + Characters, +} + +struct FoldContext<'a, W: Write> { + spaces: bool, + width: usize, + mode: WidthMode, + writer: &'a mut W, + output: &'a mut Vec, + col_count: &'a mut usize, + last_space: &'a mut Option, +} + #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let args = args.collect_lossy(); @@ -34,6 +52,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?; let bytes = matches.get_flag(options::BYTES); + let characters = matches.get_flag(options::CHARACTERS); let spaces = matches.get_flag(options::SPACES); let poss_width = match matches.get_one::(options::WIDTH) { Some(v) => Some(v.clone()), @@ -55,7 +74,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { None => vec!["-".to_owned()], }; - fold(&files, bytes, spaces, width) + fold(&files, bytes, characters, spaces, width) } pub fn uu_app() -> Command { @@ -72,6 +91,13 @@ pub fn uu_app() -> Command { .help(translate!("fold-bytes-help")) .action(ArgAction::SetTrue), ) + .arg( + Arg::new(options::CHARACTERS) + .long(options::CHARACTERS) + .help(translate!("fold-characters-help")) + .conflicts_with(options::BYTES) + .action(ArgAction::SetTrue), + ) .arg( Arg::new(options::SPACES) .long(options::SPACES) @@ -107,7 +133,13 @@ fn handle_obsolete(args: &[String]) -> (Vec, Option) { (args.to_vec(), None) } -fn fold(filenames: &[String], bytes: bool, spaces: bool, width: usize) -> UResult<()> { +fn fold( + filenames: &[String], + bytes: bool, + characters: bool, + spaces: bool, + width: usize, +) -> UResult<()> { let mut output = BufWriter::new(stdout()); for filename in filenames { @@ -125,7 +157,12 @@ fn fold(filenames: &[String], bytes: bool, spaces: bool, width: usize) -> UResul if bytes { fold_file_bytewise(buffer, spaces, width, &mut output)?; } else { - fold_file(buffer, spaces, width, &mut output)?; + let mode = if characters { + WidthMode::Characters + } else { + WidthMode::Columns + }; + fold_file(buffer, spaces, width, mode, &mut output)?; } } @@ -213,6 +250,303 @@ fn fold_file_bytewise( Ok(()) } +fn next_tab_stop(col_count: usize) -> usize { + col_count + TAB_WIDTH - col_count % TAB_WIDTH +} + +fn compute_col_count(buffer: &[u8], mode: WidthMode) -> usize { + match mode { + WidthMode::Characters => std::str::from_utf8(buffer) + .map(|s| s.chars().count()) + .unwrap_or(buffer.len()), + WidthMode::Columns => { + if let Ok(s) = std::str::from_utf8(buffer) { + let mut width = 0; + for ch in s.chars() { + match ch { + '\r' => width = 0, + '\t' => width = next_tab_stop(width), + '\x08' => width = width.saturating_sub(1), + _ => width += UnicodeWidthChar::width(ch).unwrap_or(0), + } + } + width + } else { + let mut width = 0; + for &byte in buffer { + match byte { + CR => width = 0, + TAB => width = next_tab_stop(width), + 0x08 => width = width.saturating_sub(1), + _ => width += 1, + } + } + width + } + } + } +} + +fn emit_output(ctx: &mut FoldContext<'_, W>) -> UResult<()> { + let consume = match *ctx.last_space { + Some(index) => index + 1, + None => ctx.output.len(), + }; + + if consume > 0 { + ctx.writer.write_all(&ctx.output[..consume])?; + } + ctx.writer.write_all(&[NL])?; + + let last_space = *ctx.last_space; + + if consume < ctx.output.len() { + ctx.output.drain(..consume); + } else { + ctx.output.clear(); + } + + *ctx.col_count = compute_col_count(ctx.output, ctx.mode); + + if ctx.spaces { + *ctx.last_space = last_space.and_then(|idx| { + if idx < consume { + None + } else { + Some(idx - consume) + } + }); + } else { + *ctx.last_space = None; + } + Ok(()) +} + +fn process_ascii_line(line: &[u8], ctx: &mut FoldContext<'_, W>) -> UResult<()> { + let mut idx = 0; + let len = line.len(); + + while idx < len { + match line[idx] { + NL => { + *ctx.last_space = None; + emit_output(ctx)?; + break; + } + CR => { + ctx.output.push(CR); + *ctx.col_count = 0; + idx += 1; + } + 0x08 => { + ctx.output.push(0x08); + *ctx.col_count = ctx.col_count.saturating_sub(1); + idx += 1; + } + TAB if ctx.mode == WidthMode::Columns => { + loop { + let next_stop = next_tab_stop(*ctx.col_count); + if next_stop > ctx.width && !ctx.output.is_empty() { + emit_output(ctx)?; + continue; + } + *ctx.col_count = next_stop; + break; + } + if ctx.spaces { + *ctx.last_space = Some(ctx.output.len()); + } else { + *ctx.last_space = None; + } + ctx.output.push(TAB); + idx += 1; + } + 0x00..=0x07 | 0x0B..=0x0C | 0x0E..=0x1F | 0x7F => { + ctx.output.push(line[idx]); + if ctx.spaces && line[idx].is_ascii_whitespace() && line[idx] != CR { + *ctx.last_space = Some(ctx.output.len() - 1); + } else if !ctx.spaces { + *ctx.last_space = None; + } + idx += 1; + } + _ => { + let start = idx; + while idx < len + && !matches!( + line[idx], + NL | CR | TAB | 0x08 | 0x00..=0x07 | 0x0B..=0x0C | 0x0E..=0x1F | 0x7F + ) + { + idx += 1; + } + push_ascii_segment(&line[start..idx], ctx)?; + } + } + } + + Ok(()) +} + +fn push_ascii_segment(segment: &[u8], ctx: &mut FoldContext<'_, W>) -> UResult<()> { + if segment.is_empty() { + return Ok(()); + } + + let mut remaining = segment; + + while !remaining.is_empty() { + if *ctx.col_count >= ctx.width { + emit_output(ctx)?; + continue; + } + + let available = ctx.width - *ctx.col_count; + let take = remaining.len().min(available); + let base_len = ctx.output.len(); + + ctx.output.extend_from_slice(&remaining[..take]); + *ctx.col_count += take; + + if ctx.spaces { + if let Some(pos) = remaining[..take] + .iter() + .rposition(|b| b.is_ascii_whitespace() && *b != CR) + { + *ctx.last_space = Some(base_len + pos); + } + } else { + *ctx.last_space = None; + } + + remaining = &remaining[take..]; + } + + Ok(()) +} + +fn process_utf8_line(line: &str, ctx: &mut FoldContext<'_, W>) -> UResult<()> { + if line.is_ascii() { + return process_ascii_line(line.as_bytes(), ctx); + } + + let line_bytes = line.as_bytes(); + let mut iter = line.char_indices().peekable(); + + while let Some((byte_idx, ch)) = iter.next() { + let next_idx = iter.peek().map(|(idx, _)| *idx).unwrap_or(line_bytes.len()); + + if ch == '\n' { + *ctx.last_space = None; + emit_output(ctx)?; + break; + } + + if *ctx.col_count >= ctx.width { + emit_output(ctx)?; + } + + if ch == '\r' { + ctx.output + .extend_from_slice(&line_bytes[byte_idx..next_idx]); + *ctx.col_count = 0; + continue; + } + + if ch == '\x08' { + ctx.output + .extend_from_slice(&line_bytes[byte_idx..next_idx]); + *ctx.col_count = ctx.col_count.saturating_sub(1); + continue; + } + + if ctx.mode == WidthMode::Columns && ch == '\t' { + loop { + let next_stop = next_tab_stop(*ctx.col_count); + if next_stop > ctx.width && !ctx.output.is_empty() { + emit_output(ctx)?; + continue; + } + *ctx.col_count = next_stop; + break; + } + if ctx.spaces { + *ctx.last_space = Some(ctx.output.len()); + } else { + *ctx.last_space = None; + } + ctx.output + .extend_from_slice(&line_bytes[byte_idx..next_idx]); + continue; + } + + let added = match ctx.mode { + WidthMode::Columns => UnicodeWidthChar::width(ch).unwrap_or(0), + WidthMode::Characters => 1, + }; + + if ctx.mode == WidthMode::Columns + && added > 0 + && *ctx.col_count + added > ctx.width + && !ctx.output.is_empty() + { + emit_output(ctx)?; + } + + if ctx.spaces && ch.is_ascii_whitespace() { + *ctx.last_space = Some(ctx.output.len()); + } + + ctx.output + .extend_from_slice(&line_bytes[byte_idx..next_idx]); + *ctx.col_count = ctx.col_count.saturating_add(added); + } + + Ok(()) +} + +fn process_non_utf8_line(line: &[u8], ctx: &mut FoldContext<'_, W>) -> UResult<()> { + for &byte in line { + if byte == NL { + *ctx.last_space = None; + emit_output(ctx)?; + break; + } + + if *ctx.col_count >= ctx.width { + emit_output(ctx)?; + } + + match byte { + CR => *ctx.col_count = 0, + TAB => { + let next_stop = next_tab_stop(*ctx.col_count); + if next_stop > ctx.width && !ctx.output.is_empty() { + emit_output(ctx)?; + } + *ctx.col_count = next_stop; + *ctx.last_space = if ctx.spaces { + Some(ctx.output.len()) + } else { + None + }; + ctx.output.push(byte); + continue; + } + 0x08 => *ctx.col_count = ctx.col_count.saturating_sub(1), + _ if ctx.spaces && byte.is_ascii_whitespace() => { + *ctx.last_space = Some(ctx.output.len()); + *ctx.col_count = ctx.col_count.saturating_add(1); + } + _ => *ctx.col_count = ctx.col_count.saturating_add(1), + } + + ctx.output.push(byte); + } + + Ok(()) +} + /// Fold `file` to fit `width` (number of columns). /// /// By default `fold` treats tab, backspace, and carriage return specially: @@ -226,6 +560,7 @@ fn fold_file( mut file: BufReader, spaces: bool, width: usize, + mode: WidthMode, writer: &mut W, ) -> UResult<()> { let mut line = Vec::new(); @@ -233,30 +568,6 @@ fn fold_file( let mut col_count = 0; let mut last_space = None; - /// Print the output line, resetting the column and character counts. - /// - /// If `spaces` is `true`, print the output line up to the last - /// encountered whitespace character (inclusive) and set the remaining - /// characters as the start of the next line. - macro_rules! emit_output { - () => { - let consume = match last_space { - Some(i) => i + 1, - None => output.len(), - }; - - writer.write_all(&output[..consume])?; - writer.write_all(&[NL])?; - output.drain(..consume); - - // we know there are no tabs left in output, so each char counts - // as 1 column - col_count = output.len(); - - last_space = None; - }; - } - loop { if file .read_until(NL, &mut line) @@ -266,50 +577,27 @@ fn fold_file( break; } - for ch in &line { - if *ch == NL { - // make sure to _not_ split output at whitespace, since we - // know the entire output will fit - last_space = None; - emit_output!(); - break; - } + let mut ctx = FoldContext { + spaces, + width, + mode, + writer, + output: &mut output, + col_count: &mut col_count, + last_space: &mut last_space, + }; - if col_count >= width { - emit_output!(); - } - - match *ch { - CR => col_count = 0, - TAB => { - let next_tab_stop = col_count + TAB_WIDTH - col_count % TAB_WIDTH; - - if next_tab_stop > width && !output.is_empty() { - emit_output!(); - } - - col_count = next_tab_stop; - last_space = if spaces { Some(output.len()) } else { None }; - } - 0x08 => { - col_count = col_count.saturating_sub(1); - } - _ if spaces && ch.is_ascii_whitespace() => { - last_space = Some(output.len()); - col_count += 1; - } - _ => col_count += 1, - } - - output.push(*ch); + match std::str::from_utf8(&line) { + Ok(s) => process_utf8_line(s, &mut ctx)?, + Err(_) => process_non_utf8_line(&line, &mut ctx)?, } - if !output.is_empty() { - writer.write_all(&output)?; - output.truncate(0); - } + line.clear(); + } - line.truncate(0); + if !output.is_empty() { + writer.write_all(&output)?; + output.clear(); } Ok(()) diff --git a/tests/by-util/test_fold.rs b/tests/by-util/test_fold.rs index 4a2d381fa..04072ab15 100644 --- a/tests/by-util/test_fold.rs +++ b/tests/by-util/test_fold.rs @@ -41,6 +41,24 @@ fn test_default_wrap_with_newlines() { .stdout_is_fixture("lorem_ipsum_new_line_80_column.expected"); } +#[test] +fn test_wide_characters_in_column_mode() { + new_ucmd!() + .args(&["-w", "5"]) + .pipe_in("\u{B250}\u{B250}\u{B250}\n") + .succeeds() + .stdout_is("\u{B250}\u{B250}\n\u{B250}\n"); +} + +#[test] +fn test_wide_characters_with_characters_option() { + new_ucmd!() + .args(&["--characters", "-w", "5"]) + .pipe_in("\u{B250}\u{B250}\u{B250}\n") + .succeeds() + .stdout_is("\u{B250}\u{B250}\u{B250}\n"); +} + #[test] fn test_should_preserve_empty_line_without_final_newline() { new_ucmd!() From baf0a737f7f7db3427ba675c8e75cac46cf7a85c Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Wed, 12 Nov 2025 09:40:01 +0100 Subject: [PATCH 038/182] ci: remove commented out line from freebsd.yml --- .github/workflows/freebsd.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/freebsd.yml b/.github/workflows/freebsd.yml index e75523b50..89a9a6f18 100644 --- a/.github/workflows/freebsd.yml +++ b/.github/workflows/freebsd.yml @@ -160,7 +160,6 @@ jobs: WORKSPACE="${WORKSPACE_PARENT}/${REPO_NAME}" # pw adduser -n ${TEST_USER} -d /root/ -g wheel -c "Coreutils user to build" -w random - # chown -R ${TEST_USER}:wheel /root/ "${WORKSPACE_PARENT}"/ chown -R ${TEST_USER}:wheel /root/ "${WORKSPACE_PARENT}"/ whoami # From ceeacf4a5fe34cb0aa50786a3c5fb97d633fe595 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Thu, 13 Nov 2025 22:53:23 +0900 Subject: [PATCH 039/182] GnuTests.yml: reduce deps (#9259) * GnuTests.yml: reduce deps * GnuTests.yml: cleanup deps https://github.com/actions/runner-images/blob/main/images/ubuntu/Ubuntu2204-Readme.md * GnuTests.yml: drop g++ --- .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 93716281f..a089b6b78 100644 --- a/.github/workflows/GnuTests.yml +++ b/.github/workflows/GnuTests.yml @@ -80,7 +80,7 @@ jobs: run: | ## Install dependencies sudo apt-get update - sudo apt-get install -y autoconf autopoint bison texinfo gperf gcc g++ gdb python3-pyinotify jq valgrind libexpect-perl libacl1-dev libattr1-dev libcap-dev libselinux1-dev attr quilt + sudo apt-get install -y autopoint gperf gdb python3-pyinotify valgrind libexpect-perl libacl1-dev libattr1-dev libcap-dev libselinux1-dev attr quilt - name: Add various locales shell: bash run: | @@ -235,7 +235,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 g++ gdb jq libacl-devel libattr-devel libcap-devel libselinux-devel attr rustup clang-devel texinfo-tex wget automake patch quilt + 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 wget automake patch quilt lima rustup-init -y --default-toolchain stable - name: Copy the sources to VM run: | From 913fe842244314d348f36ddceed31f7f6a50a6bc Mon Sep 17 00:00:00 2001 From: oech3 <> Date: Thu, 13 Nov 2025 18:45:04 +0900 Subject: [PATCH 040/182] Avoid mixing wget and curl --- GNUmakefile | 4 ++-- src/uu/head/BENCHMARKING.md | 2 +- src/uu/shuf/BENCHMARKING.md | 2 +- src/uu/wc/BENCHMARKING.md | 4 ++-- util/android-commands.sh | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/GNUmakefile b/GNUmakefile index b13dbbd34..b2deceed6 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -345,7 +345,7 @@ test_toybox: toybox-src: if [ ! -e "$(TOYBOX_SRC)" ] ; then \ mkdir -p "$(TOYBOX_ROOT)" ; \ - wget "https://github.com/landley/toybox/archive/refs/tags/$(TOYBOX_VER).tar.gz" -P "$(TOYBOX_ROOT)" ; \ + curl "https://github.com/landley/toybox/archive/refs/tags/$(TOYBOX_VER).tar.gz" -o "$(TOYBOX_ROOT)/$(TOYBOX_VER).tar.gz" ; \ tar -C "$(TOYBOX_ROOT)" -xf "$(TOYBOX_ROOT)/$(TOYBOX_VER).tar.gz" ; \ sed -i -e "s|TESTDIR=\".*\"|TESTDIR=\"$(BUILDDIR)\"|g" $(TOYBOX_SRC)/scripts/test.sh; \ sed -i -e "s/ || exit 1//g" $(TOYBOX_SRC)/scripts/test.sh; \ @@ -354,7 +354,7 @@ toybox-src: busybox-src: if [ ! -e "$(BUSYBOX_SRC)" ] ; then \ mkdir -p "$(BUSYBOX_ROOT)" ; \ - wget "https://busybox.net/downloads/busybox-$(BUSYBOX_VER).tar.bz2" -P "$(BUSYBOX_ROOT)" ; \ + curl "https://busybox.net/downloads/busybox-$(BUSYBOX_VER).tar.bz2" -o "$(BUSYBOX_ROOT)/$(BUSYBOX_VER).tar.bz2" ; \ tar -C "$(BUSYBOX_ROOT)" -xf "$(BUSYBOX_ROOT)/busybox-$(BUSYBOX_VER).tar.bz2" ; \ fi ; diff --git a/src/uu/head/BENCHMARKING.md b/src/uu/head/BENCHMARKING.md index d751d1f7f..79b6821e5 100644 --- a/src/uu/head/BENCHMARKING.md +++ b/src/uu/head/BENCHMARKING.md @@ -20,7 +20,7 @@ William Shakespeare*, which is in the public domain in the United States and most other parts of the world. ```shell -wget -O shakespeare.txt https://www.gutenberg.org/files/100/100-0.txt +curl -o shakespeare.txt https://www.gutenberg.org/files/100/100-0.txt ``` This particular file has about 170,000 lines, each of which is no longer diff --git a/src/uu/shuf/BENCHMARKING.md b/src/uu/shuf/BENCHMARKING.md index d16b1afb0..ddb98b457 100644 --- a/src/uu/shuf/BENCHMARKING.md +++ b/src/uu/shuf/BENCHMARKING.md @@ -14,7 +14,7 @@ renaming the executable from `shuf` to `shuf.old`. Sample input can be generated using `/dev/random`: ```shell -wget -O input.txt https://www.gutenberg.org/files/100/100-0.txt +curl -o input.txt https://www.gutenberg.org/files/100/100-0.txt ``` To avoid distortions from IO, it is recommended to store input data in tmpfs. diff --git a/src/uu/wc/BENCHMARKING.md b/src/uu/wc/BENCHMARKING.md index 6c938a602..60f9139da 100644 --- a/src/uu/wc/BENCHMARKING.md +++ b/src/uu/wc/BENCHMARKING.md @@ -56,7 +56,7 @@ To get a file with less artificial contents, download a book from Project Gutenberg and concatenate it a lot of times: ```shell -wget https://www.gutenberg.org/files/2701/2701-0.txt -O moby.txt +curl https://www.gutenberg.org/files/2701/2701-0.txt -o moby.txt cat moby.txt moby.txt moby.txt moby.txt > moby4.txt cat moby4.txt moby4.txt moby4.txt moby4.txt > moby16.txt cat moby16.txt moby16.txt moby16.txt moby16.txt > moby64.txt @@ -65,7 +65,7 @@ cat moby16.txt moby16.txt moby16.txt moby16.txt > moby64.txt And get one with lots of unicode too: ```shell -wget https://www.gutenberg.org/files/30613/30613-0.txt -O odyssey.txt +curl https://www.gutenberg.org/files/30613/30613-0.txt -o odyssey.txt cat odyssey.txt odyssey.txt odyssey.txt odyssey.txt > odyssey4.txt cat odyssey4.txt odyssey4.txt odyssey4.txt odyssey4.txt > odyssey16.txt cat odyssey16.txt odyssey16.txt odyssey16.txt odyssey16.txt > odyssey64.txt diff --git a/util/android-commands.sh b/util/android-commands.sh index aa02e5b34..b87d7050b 100755 --- a/util/android-commands.sh +++ b/util/android-commands.sh @@ -329,7 +329,7 @@ init() { snapshot_name="${AVD_CACHE_KEY}" # shellcheck disable=SC2015 - wget -nv "https://github.com/termux/termux-app/releases/download/${termux}/termux-app_${termux}+github-debug_${arch}.apk" && + curl -sLO "https://github.com/termux/termux-app/releases/download/${termux}/termux-app_${termux}+github-debug_${arch}.apk" && snapshot "termux-app_${termux}+github-debug_${arch}.apk" && hash_rustc && exit_termux && From ff92c55962701ef3047d471a543a205dc39e20e7 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Thu, 13 Nov 2025 20:15:46 +0900 Subject: [PATCH 041/182] GNUmakefile:curl -Ls --- GNUmakefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GNUmakefile b/GNUmakefile index b2deceed6..010933bf9 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -345,7 +345,7 @@ test_toybox: toybox-src: if [ ! -e "$(TOYBOX_SRC)" ] ; then \ mkdir -p "$(TOYBOX_ROOT)" ; \ - curl "https://github.com/landley/toybox/archive/refs/tags/$(TOYBOX_VER).tar.gz" -o "$(TOYBOX_ROOT)/$(TOYBOX_VER).tar.gz" ; \ + curl -Ls "https://github.com/landley/toybox/archive/refs/tags/$(TOYBOX_VER).tar.gz" -o "$(TOYBOX_ROOT)/$(TOYBOX_VER).tar.gz" ; \ tar -C "$(TOYBOX_ROOT)" -xf "$(TOYBOX_ROOT)/$(TOYBOX_VER).tar.gz" ; \ sed -i -e "s|TESTDIR=\".*\"|TESTDIR=\"$(BUILDDIR)\"|g" $(TOYBOX_SRC)/scripts/test.sh; \ sed -i -e "s/ || exit 1//g" $(TOYBOX_SRC)/scripts/test.sh; \ @@ -354,7 +354,7 @@ toybox-src: busybox-src: if [ ! -e "$(BUSYBOX_SRC)" ] ; then \ mkdir -p "$(BUSYBOX_ROOT)" ; \ - curl "https://busybox.net/downloads/busybox-$(BUSYBOX_VER).tar.bz2" -o "$(BUSYBOX_ROOT)/$(BUSYBOX_VER).tar.bz2" ; \ + curl -Ls "https://busybox.net/downloads/busybox-$(BUSYBOX_VER).tar.bz2" -o "$(BUSYBOX_ROOT)/$(BUSYBOX_VER).tar.bz2" ; \ tar -C "$(BUSYBOX_ROOT)" -xf "$(BUSYBOX_ROOT)/busybox-$(BUSYBOX_VER).tar.bz2" ; \ fi ; From ea1298096986812c31e56124bd6bfb41a75ae968 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Fri, 14 Nov 2025 00:19:39 +0900 Subject: [PATCH 042/182] GNUmakefile: DL busybox fro mirror (better resp) --- GNUmakefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GNUmakefile b/GNUmakefile index 010933bf9..f262c50ea 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -354,8 +354,8 @@ toybox-src: busybox-src: if [ ! -e "$(BUSYBOX_SRC)" ] ; then \ mkdir -p "$(BUSYBOX_ROOT)" ; \ - curl -Ls "https://busybox.net/downloads/busybox-$(BUSYBOX_VER).tar.bz2" -o "$(BUSYBOX_ROOT)/$(BUSYBOX_VER).tar.bz2" ; \ - tar -C "$(BUSYBOX_ROOT)" -xf "$(BUSYBOX_ROOT)/busybox-$(BUSYBOX_VER).tar.bz2" ; \ + curl -Ls "https://github.com/mirror/busybox/archive/refs/tags/$(subst .,_,$(BUSYBOX_VER)).tar.gz" -o "$(BUSYBOX_ROOT)/busybox-$(BUSYBOX_VER).tar.gz" ; \ + tar -C "$(BUSYBOX_ROOT)" -xf "$(BUSYBOX_ROOT)/busybox-$(BUSYBOX_VER).tar.gz" ; \ fi ; # This is a busybox-specific config file their test suite wants to parse. From e502d894dc6534f77b94f89abbec550b8dc6c892 Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Wed, 12 Nov 2025 16:18:18 +0100 Subject: [PATCH 043/182] test: existing file is newer than non-existing existing -nt non-existing => true non-existing -nt existing => false existing -ot non-existing => false non-existing -ot existing => true --- Cargo.lock | 1 + src/uu/test/Cargo.toml | 9 ++-- src/uu/test/src/test.rs | 107 +++++++++++++++++++++++++++++++------ tests/by-util/test_test.rs | 30 +++++++---- 4 files changed, 118 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f2f8412cc..fa3a3018e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4104,6 +4104,7 @@ dependencies = [ "clap", "fluent", "libc", + "tempfile", "thiserror 2.0.17", "uucore", ] diff --git a/src/uu/test/Cargo.toml b/src/uu/test/Cargo.toml index 1785063a0..3348efd7b 100644 --- a/src/uu/test/Cargo.toml +++ b/src/uu/test/Cargo.toml @@ -19,10 +19,13 @@ path = "src/test.rs" [dependencies] clap = { workspace = true } -libc = { workspace = true } -uucore = { workspace = true, features = ["process"] } -thiserror = { workspace = true } fluent = { workspace = true } +libc = { workspace = true } +thiserror = { workspace = true } +uucore = { workspace = true, features = ["process"] } + +[dev-dependencies] +tempfile = { workspace = true } [[bin]] name = "test" diff --git a/src/uu/test/src/test.rs b/src/uu/test/src/test.rs index 34d594241..0e4e809d7 100644 --- a/src/uu/test/src/test.rs +++ b/src/uu/test/src/test.rs @@ -205,24 +205,26 @@ fn integers(a: &OsStr, b: &OsStr, op: &OsStr) -> ParseResult { /// Operations to compare files metadata /// `a` is the left hand side -/// `b` is the left hand side +/// `b` is the right hand side /// `op` the operation (ex: -ef, -nt, etc) fn files(a: &OsStr, b: &OsStr, op: &OsStr) -> ParseResult { - // Don't manage the error. GNU doesn't show error when doing - // test foo -nt bar - let (Ok(f_a), Ok(f_b)) = (fs::metadata(a), fs::metadata(b)) else { - return Ok(false); + let f_a = fs::metadata(a); + let f_b = fs::metadata(b); + + let result = match (op.to_str(), f_a, f_b) { + #[cfg(unix)] + (Some("-ef"), Ok(f_a), Ok(f_b)) => f_a.ino() == f_b.ino() && f_a.dev() == f_b.dev(), + #[cfg(not(unix))] + (Some("-ef"), Ok(_), Ok(_)) => unimplemented!(), + (Some("-nt"), Ok(f_a), Ok(f_b)) => f_a.modified().unwrap() > f_b.modified().unwrap(), + (Some("-nt"), Ok(_), _) => true, + (Some("-ot"), Ok(f_a), Ok(f_b)) => f_a.modified().unwrap() < f_b.modified().unwrap(), + (Some("-ot"), _, Ok(_)) => true, + (Some("-ef" | "-nt" | "-ot"), _, _) => false, + (_, _, _) => return Err(ParseError::UnknownOperator(op.quote().to_string())), }; - Ok(match op.to_str() { - #[cfg(unix)] - Some("-ef") => f_a.ino() == f_b.ino() && f_a.dev() == f_b.dev(), - #[cfg(not(unix))] - Some("-ef") => unimplemented!(), - Some("-nt") => f_a.modified().unwrap() > f_b.modified().unwrap(), - Some("-ot") => f_a.modified().unwrap() < f_b.modified().unwrap(), - _ => return Err(ParseError::UnknownOperator(op.quote().to_string())), - }) + Ok(result) } fn isatty(fd: &OsStr) -> ParseResult { @@ -347,8 +349,81 @@ fn path(path: &OsStr, condition: &PathCondition) -> bool { #[cfg(test)] mod tests { - use super::integers; - use std::ffi::OsStr; + use super::*; + use std::{ffi::OsStr, time::UNIX_EPOCH}; + use tempfile::NamedTempFile; + + #[test] + fn test_files_with_unknown_op() { + let a = NamedTempFile::new().unwrap(); + let b = NamedTempFile::new().unwrap(); + let a = OsStr::new(a.path()); + let b = OsStr::new(b.path()); + let op = OsStr::new("unknown_op"); + + assert!(files(a, b, op).is_err()); + } + + #[test] + #[cfg(unix)] + fn test_files_with_ef_op() { + let a = NamedTempFile::new().unwrap(); + let b = NamedTempFile::new().unwrap(); + let a = OsStr::new(a.path()); + let b = OsStr::new(b.path()); + let op = OsStr::new("-ef"); + + assert!(files(a, a, op).unwrap()); + assert!(!files(a, b, op).unwrap()); + assert!(!files(b, a, op).unwrap()); + + let existing_file = a; + let non_existing_file = OsStr::new("non_existing_file"); + + assert!(!files(existing_file, non_existing_file, op).unwrap()); + assert!(!files(non_existing_file, existing_file, op).unwrap()); + assert!(!files(non_existing_file, non_existing_file, op).unwrap()); + } + + #[test] + fn test_files_with_nt_op() { + let older_file = NamedTempFile::new().unwrap(); + older_file.as_file().set_modified(UNIX_EPOCH).unwrap(); + let older_file = OsStr::new(older_file.path()); + let newer_file = NamedTempFile::new().unwrap(); + let newer_file = OsStr::new(newer_file.path()); + let op = OsStr::new("-nt"); + + assert!(files(newer_file, older_file, op).unwrap()); + assert!(!files(older_file, newer_file, op).unwrap()); + + let existing_file = newer_file; + let non_existing_file = OsStr::new("non_existing_file"); + + assert!(files(existing_file, non_existing_file, op).unwrap()); + assert!(!files(non_existing_file, existing_file, op).unwrap()); + assert!(!files(non_existing_file, non_existing_file, op).unwrap()); + } + + #[test] + fn test_files_with_ot_op() { + let older_file = NamedTempFile::new().unwrap(); + older_file.as_file().set_modified(UNIX_EPOCH).unwrap(); + let older_file = OsStr::new(older_file.path()); + let newer_file = NamedTempFile::new().unwrap(); + let newer_file = OsStr::new(newer_file.path()); + let op = OsStr::new("-ot"); + + assert!(!files(newer_file, older_file, op).unwrap()); + assert!(files(older_file, newer_file, op).unwrap()); + + let existing_file = newer_file; + let non_existing_file = OsStr::new("non_existing_file"); + + assert!(!files(existing_file, non_existing_file, op).unwrap()); + assert!(files(non_existing_file, existing_file, op).unwrap()); + assert!(!files(non_existing_file, non_existing_file, op).unwrap()); + } #[test] fn test_integer_op() { diff --git a/tests/by-util/test_test.rs b/tests/by-util/test_test.rs index 1dba782f5..4b5460cfd 100644 --- a/tests/by-util/test_test.rs +++ b/tests/by-util/test_test.rs @@ -5,10 +5,8 @@ // spell-checker:ignore (words) egid euid pseudofloat -use uutests::at_and_ucmd; -use uutests::new_ucmd; use uutests::util::TestScenario; -use uutests::util_name; +use uutests::{at_and_ucmd, new_ucmd, util_name}; #[test] fn test_empty_test_equivalent_to_false() { @@ -337,14 +335,26 @@ fn test_file_is_newer_than_and_older_than_itself() { } #[test] -fn test_non_existing_files() { - let scenario = TestScenario::new(util_name!()); +fn test_file_is_newer_than_non_existing_file() { + new_ucmd!() + .args(&["non_existing_file", "-nt", "regular_file"]) + .fails_with_code(1) + .no_output(); - let result = scenario - .ucmd() - .args(&["newer_file", "-nt", "regular_file"]) - .fails_with_code(1); - assert!(result.stderr().is_empty()); + new_ucmd!() + .args(&["regular_file", "-nt", "non_existing_file"]) + .succeeds() + .no_output(); + + new_ucmd!() + .args(&["non_existing_file", "-ot", "regular_file"]) + .succeeds() + .no_output(); + + new_ucmd!() + .args(&["regular_file", "-ot", "non_existing_file"]) + .fails_with_code(1) + .no_output(); } #[test] From fa82066ceed07588771a3dbe22202b9e95ecb56b Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Wed, 12 Nov 2025 22:42:47 +0100 Subject: [PATCH 044/182] pr: fix header formatting for custom date formats starting with '+' Should fix tests/misc/time-style.sh --- src/uu/pr/src/pr.rs | 70 +++++++++++++++++++++++++++++++--------- tests/by-util/test_pr.rs | 43 ++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 16 deletions(-) diff --git a/src/uu/pr/src/pr.rs b/src/uu/pr/src/pr.rs index f21f69732..f5c5662aa 100644 --- a/src/uu/pr/src/pr.rs +++ b/src/uu/pr/src/pr.rs @@ -1150,23 +1150,61 @@ fn get_formatted_line_number(opts: &OutputOptions, line_number: usize, index: us /// Returns a five line header content if displaying header is not disabled by /// using `NO_HEADER_TRAILER_OPTION` option. fn header_content(options: &OutputOptions, page: usize) -> Vec { - if options.display_header_and_trailer { - let first_line = format!( - "{} {} {} {page}", - options.last_modified_time, - options.header, - translate!("pr-page") - ); - vec![ - String::new(), - String::new(), - first_line, - String::new(), - String::new(), - ] - } else { - Vec::new() + if !options.display_header_and_trailer { + return Vec::new(); } + + // The header should be formatted with proper spacing: + // - Date/time on the left + // - Filename centered + // - "Page X" on the right + let date_part = &options.last_modified_time; + let filename = &options.header; + let page_part = format!("{} {page}", translate!("pr-page")); + + // 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}") + } + } else { + // If content is too long, just use single spaces + format!("{date_part} {filename} {page_part}") + }; + + vec![ + String::new(), + String::new(), + header_line, + String::new(), + String::new(), + ] } /// Returns five empty lines as trailer content if displaying trailer diff --git a/tests/by-util/test_pr.rs b/tests/by-util/test_pr.rs index 0d1c4bc4b..1fa91dab2 100644 --- a/tests/by-util/test_pr.rs +++ b/tests/by-util/test_pr.rs @@ -558,6 +558,49 @@ fn test_value_for_number_lines() { new_ucmd!().args(&["-n", "foo5.txt", "test.log"]).fails(); } +#[test] +fn test_header_formatting_with_custom_date_format() { + // This test verifies that the header is properly formatted with: + // - Date/time on the left + // - Filename centered + // - "Page X" on the right + // This matches GNU pr behavior for the time-style test + + let test_file_path = "test_one_page.log"; + + // Set a specific date format like in the GNU test + let output = new_ucmd!() + .args(&["-D", "+%Y-%m-%d %H:%M:%S %z (%Z)", test_file_path]) + .succeeds() + .stdout_move_str(); + + // Extract the header line (3rd line of output) + let lines: Vec<&str> = output.lines().collect(); + assert!( + lines.len() >= 5, + "Output should have at least 5 lines for header" + ); + + let header_line = lines[2]; + + // The header should be 72 characters wide (default page width) + assert_eq!(header_line.chars().count(), 72); + + // Check that it contains the expected parts + assert!(header_line.contains(test_file_path)); + assert!(header_line.contains("Page 1")); + + // Verify the filename is roughly centered + let filename_pos = header_line.find(test_file_path).unwrap(); + let page_pos = header_line.find("Page 1").unwrap(); + + // Filename should be somewhere in the middle third of the line + assert!(filename_pos > 24 && filename_pos < 48); + + // Page should be right-aligned (near the end) + assert!(page_pos >= 60); +} + #[test] fn test_help() { new_ucmd!().arg("--help").succeeds(); From 20031e4604bbe5a7d5b45847321a87894a367b83 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Fri, 14 Nov 2025 14:57:44 +0900 Subject: [PATCH 045/182] Update CICD.yml: Stop uploading duplicated release binary --- .github/workflows/CICD.yml | 38 +------------------------------------- 1 file changed, 1 insertion(+), 37 deletions(-) diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index f5ec1cfbc..e06156822 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -4,7 +4,7 @@ name: CICD # spell-checker:ignore (env/flags) Awarnings Ccodegen Coverflow Cpanic Dwarnings RUSTDOCFLAGS RUSTFLAGS Zpanic CARGOFLAGS # spell-checker:ignore (jargon) SHAs deps dequote softprops subshell toolchain fuzzers dedupe devel profdata # spell-checker:ignore (people) Peltoche rivy dtolnay Anson dawidd -# spell-checker:ignore (shell/tools) binutils choco clippy dmake dpkg esac fakeroot fdesc fdescfs gmake grcov halium lcov libclang libfuse libssl limactl mkdir nextest nocross pacman popd printf pushd redoxer rsync rustc rustfmt rustup shopt sccache utmpdump xargs +# spell-checker:ignore (shell/tools) binutils choco clippy dmake esac fakeroot fdesc fdescfs gmake grcov halium lcov libclang libfuse libssl limactl mkdir nextest nocross pacman popd printf pushd redoxer rsync rustc rustfmt rustup shopt sccache utmpdump xargs # spell-checker:ignore (misc) aarch alnum armhf bindir busytest coreutils defconfig DESTDIR gecos getenforce gnueabihf issuecomment maint manpages msys multisize noconfirm nofeatures nullglob onexitbegin onexitend pell runtest Swatinem tempfile testsuite toybox uutils libsystemd codspeed env: @@ -657,25 +657,6 @@ jobs: # deployable tag? (ie, leading "vM" or "M"; M == version number) unset DEPLOY ; if [[ $REF_TAG =~ ^[vV]?[0-9].* ]]; then DEPLOY='true' ; fi outputs DEPLOY - # DPKG architecture? - unset DPKG_ARCH - case ${{ matrix.job.target }} in - x86_64-*-linux-*) DPKG_ARCH=amd64 ;; - *-linux-*) DPKG_ARCH=${TARGET_ARCH} ;; - esac - outputs DPKG_ARCH - # DPKG version? - unset DPKG_VERSION ; if [[ $REF_TAG =~ ^[vV]?[0-9].* ]]; then DPKG_VERSION=${REF_TAG/#[vV]/} ; fi - outputs DPKG_VERSION - # DPKG base name/conflicts? - DPKG_BASENAME=${PROJECT_NAME} - DPKG_CONFLICTS=${PROJECT_NAME}-musl - case ${{ matrix.job.target }} in *-musl) DPKG_BASENAME=${PROJECT_NAME}-musl ; DPKG_CONFLICTS=${PROJECT_NAME} ;; esac; - outputs DPKG_BASENAME DPKG_CONFLICTS - # DPKG name - unset DPKG_NAME; - if [[ -n $DPKG_ARCH && -n $DPKG_VERSION ]]; then DPKG_NAME="${DPKG_BASENAME}_${DPKG_VERSION}_${DPKG_ARCH}.deb" ; fi - outputs DPKG_NAME # target-specific options # * CARGO_FEATURES_OPTION CARGO_FEATURES_OPTION='' ; @@ -734,7 +715,6 @@ jobs: ## Create build/work space mkdir -p '${{ steps.vars.outputs.STAGING }}' mkdir -p '${{ steps.vars.outputs.STAGING }}/${{ steps.vars.outputs.PKG_BASENAME }}' - mkdir -p '${{ steps.vars.outputs.STAGING }}/dpkg' - name: Install/setup prerequisites shell: bash run: | @@ -866,21 +846,6 @@ jobs: *) tar czf '${{ steps.vars.outputs.PKG_NAME }}' '${{ steps.vars.outputs.PKG_BASENAME }}'/* ;; esac popd >/dev/null - # dpkg - if [ -n "${{ steps.vars.outputs.DPKG_NAME }}" ]; then - DPKG_DIR="${{ steps.vars.outputs.STAGING }}/dpkg" - # binary - install -Dm755 'target/${{ matrix.job.target }}/release/${{ env.PROJECT_NAME }}${{ steps.vars.outputs.EXE_suffix }}' "${DPKG_DIR}/usr/bin/${{ env.PROJECT_NAME }}${{ steps.vars.outputs.EXE_suffix }}" - if [ -n "${{ steps.vars.outputs.STRIP }}" ]; then "${{ steps.vars.outputs.STRIP }}" "${DPKG_DIR}/usr/bin/${{ env.PROJECT_NAME }}${{ steps.vars.outputs.EXE_suffix }}" ; fi - # README and LICENSE - (shopt -s nullglob; for f in [R]"EADME"{,.*}; do install -Dm644 "$f" "${DPKG_DIR}/usr/share/doc/${{ env.PROJECT_NAME }}/$f" ; done) - (shopt -s nullglob; for f in [L]"ICENSE"{-*,}{,.*}; do install -Dm644 "$f" "${DPKG_DIR}/usr/share/doc/${{ env.PROJECT_NAME }}/$f" ; done) - # control file - mkdir -p "${DPKG_DIR}/DEBIAN" - printf "Package: ${{ steps.vars.outputs.DPKG_BASENAME }}\nVersion: ${{ steps.vars.outputs.DPKG_VERSION }}\nSection: utils\nPriority: optional\nMaintainer: ${{ env.PROJECT_AUTH }}\nArchitecture: ${{ steps.vars.outputs.DPKG_ARCH }}\nProvides: ${{ env.PROJECT_NAME }}\nConflicts: ${{ steps.vars.outputs.DPKG_CONFLICTS }}\nDescription: ${{ env.PROJECT_DESC }}\n" > "${DPKG_DIR}/DEBIAN/control" - # build dpkg - fakeroot dpkg-deb --build "${DPKG_DIR}" "${{ steps.vars.outputs.STAGING }}/${{ steps.vars.outputs.DPKG_NAME }}" - fi - name: Publish uses: softprops/action-gh-release@v2 if: steps.vars.outputs.DEPLOY && matrix.job.skip-publish != true @@ -888,7 +853,6 @@ jobs: draft: true files: | ${{ steps.vars.outputs.STAGING }}/${{ steps.vars.outputs.PKG_NAME }} - ${{ steps.vars.outputs.STAGING }}/${{ steps.vars.outputs.DPKG_NAME }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 5f4e223efed82952820a622c3d678346ded856c4 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Fri, 14 Nov 2025 13:40:28 +0900 Subject: [PATCH 046/182] README.md: profiles for binary size --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index bcd32f75d..e8072a095 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,8 @@ other Rust program: cargo build --release ``` +Replace `--release` with `--profile=release-fast` or `--profile=release-small` to use all optimizations or save binary size. + This command builds the most portable common core set of uutils into a multicall (BusyBox-type) binary, named 'coreutils', on most Rust-supported platforms. From 0d92953e0169c0787aabec69d406e2e6658e88f1 Mon Sep 17 00:00:00 2001 From: oech3 <> Date: Fri, 14 Nov 2025 16:40:47 +0900 Subject: [PATCH 047/182] build-gnu.sh: Freeze SELinux build mode --- util/build-gnu.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/util/build-gnu.sh b/util/build-gnu.sh index 3d2b509a2..cb23f0ad0 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -140,7 +140,8 @@ else # Change the PATH to test the uutils coreutils instead of the GNU coreutils sed -i "s/^[[:blank:]]*PATH=.*/ PATH='${UU_BUILD_DIR//\//\\/}\$(PATH_SEPARATOR)'\"\$\$PATH\" \\\/" tests/local.mk ./bootstrap --skip-po - ./configure --quiet --disable-gcc-warnings --disable-nls --disable-dependency-tracking --disable-bold-man-page-references + ./configure --quiet --disable-gcc-warnings --disable-nls --disable-dependency-tracking --disable-bold-man-page-references \ + "$([ ${SELINUX_ENABLED} = 1 ] && echo --with-selinux || echo --without-selinux)" #Add timeout to to protect against hangs sed -i 's|^"\$@|'"${SYSTEM_TIMEOUT}"' 600 "\$@|' build-aux/test-driver sed -i 's| tr | /usr/bin/tr |' tests/init.sh From 21c219abf7ead376ba8fe6f55cb58d3985523ada Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Fri, 14 Nov 2025 18:01:56 +0900 Subject: [PATCH 048/182] CICD.yml: Remove if for .exe --- .github/workflows/CICD.yml | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index e06156822..dd09756b0 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -1263,13 +1263,8 @@ jobs: - name: Verify stub binaries exist shell: bash run: | - if [ "${{ runner.os }}" = "Windows" ]; then - test -f target/debug/chcon.exe - test -f target/debug/runcon.exe - else - test -f target/debug/chcon - test -f target/debug/runcon - fi + test -f target/debug/chcon || test -f target/debug/chcon.exe + test -f target/debug/runcon || test -f target/debug/runcon.exe - name: Verify workspace builds with stubs run: cargo build --features ${{ matrix.job.features }} From 907c57396fb130e2d55af60f0afceb17ca8c610b Mon Sep 17 00:00:00 2001 From: naoNao89 <90588855+naoNao89@users.noreply.github.com> Date: Sat, 15 Nov 2025 00:51:09 +0700 Subject: [PATCH 049/182] numfmt: add Q/R/k suffixes and fix bugs - Add quetta (Q), ronna (R) suffix support (10^30, 10^27) - Support lowercase 'k' suffix - Fix "invalid number" vs "invalid suffix" error distinction - Fix output duplication on formatting errors - Add regression tests Improves GNU test suite compatibility. --- src/uu/numfmt/src/format.rs | 173 ++++++++++++++++++++++++++++++++--- src/uu/numfmt/src/numfmt.rs | 4 +- src/uu/numfmt/src/units.rs | 9 +- tests/by-util/test_numfmt.rs | 19 ++-- 4 files changed, 176 insertions(+), 29 deletions(-) diff --git a/src/uu/numfmt/src/format.rs b/src/uu/numfmt/src/format.rs index 816b2fda5..e091f2320 100644 --- a/src/uu/numfmt/src/format.rs +++ b/src/uu/numfmt/src/format.rs @@ -74,6 +74,7 @@ fn parse_suffix(s: &str) -> Result<(f64, Option)> { } let suffix = match iter.next_back() { Some('K') => Some((RawSuffix::K, with_i)), + Some('k') => Some((RawSuffix::K, with_i)), Some('M') => Some((RawSuffix::M, with_i)), Some('G') => Some((RawSuffix::G, with_i)), Some('T') => Some((RawSuffix::T, with_i)), @@ -81,8 +82,20 @@ fn parse_suffix(s: &str) -> Result<(f64, Option)> { Some('E') => Some((RawSuffix::E, with_i)), Some('Z') => Some((RawSuffix::Z, with_i)), Some('Y') => Some((RawSuffix::Y, with_i)), + Some('R') => Some((RawSuffix::R, with_i)), + Some('Q') => Some((RawSuffix::Q, with_i)), Some('0'..='9') if !with_i => None, _ => { + // If with_i is true, the string ends with 'i' but there's no valid suffix letter + // This is always an invalid suffix (e.g., "1i", "2Ai") + if with_i { + return Err(translate!("numfmt-error-invalid-suffix", "input" => s.quote())); + } + // For other cases, check if the number part (without the last character) is valid + let number_part = &s[..s.len() - 1]; + if number_part.is_empty() || number_part.parse::().is_err() { + return Err(translate!("numfmt-error-invalid-number", "input" => s.quote())); + } return Err(translate!("numfmt-error-invalid-suffix", "input" => s.quote())); } }; @@ -123,6 +136,8 @@ fn remove_suffix(i: f64, s: Option, u: &Unit) -> Result { RawSuffix::E => Ok(i * 1e18), RawSuffix::Z => Ok(i * 1e21), RawSuffix::Y => Ok(i * 1e24), + RawSuffix::R => Ok(i * 1e27), + RawSuffix::Q => Ok(i * 1e30), }, (Some((raw_suffix, false)), &Unit::Iec(false)) | (Some((raw_suffix, true)), &Unit::Auto | &Unit::Iec(true)) => match raw_suffix { @@ -134,6 +149,8 @@ fn remove_suffix(i: f64, s: Option, u: &Unit) -> Result { RawSuffix::E => Ok(i * IEC_BASES[6]), RawSuffix::Z => Ok(i * IEC_BASES[7]), RawSuffix::Y => Ok(i * IEC_BASES[8]), + RawSuffix::R => Ok(i * IEC_BASES[9]), + RawSuffix::Q => Ok(i * IEC_BASES[10]), }, (Some((raw_suffix, false)), &Unit::Iec(true)) => Err( translate!("numfmt-error-missing-i-suffix", "number" => i, "suffix" => format!("{raw_suffix:?}")), @@ -212,10 +229,10 @@ fn consider_suffix( round_method: RoundMethod, precision: usize, ) -> Result<(f64, Option)> { - use crate::units::RawSuffix::{E, G, K, M, P, T, Y, Z}; + use crate::units::RawSuffix::{E, G, K, M, P, Q, R, T, Y, Z}; let abs_n = n.abs(); - let suffixes = [K, M, G, T, P, E, Z, Y]; + let suffixes = [K, M, G, T, P, E, Z, Y, R, Q]; let (bases, with_i) = match *u { Unit::Si => (&SI_BASES, false), @@ -234,6 +251,8 @@ fn consider_suffix( _ if abs_n < bases[7] => 6, _ if abs_n < bases[8] => 7, _ if abs_n < bases[9] => 8, + _ if abs_n < bases[10] => 9, + _ if abs_n < bases[10] * 1000.0 => 10, _ => return Err(translate!("numfmt-error-number-too-big")), }; @@ -334,38 +353,41 @@ fn format_string( fn format_and_print_delimited(s: &str, options: &NumfmtOptions) -> Result<()> { let delimiter = options.delimiter.as_ref().unwrap(); + let mut output = String::new(); for (n, field) in (1..).zip(s.split(delimiter)) { let field_selected = uucore::ranges::contain(&options.fields, n); - // print delimiter before second and subsequent fields + // add delimiter before second and subsequent fields if n > 1 { - print!("{delimiter}"); + output.push_str(delimiter); } if field_selected { - print!("{}", format_string(field.trim_start(), options, None)?); + output.push_str(&format_string(field.trim_start(), options, None)?); } else { - // print unselected field without conversion - print!("{field}"); + // add unselected field without conversion + output.push_str(field); } } - println!(); + println!("{output}"); Ok(()) } fn format_and_print_whitespace(s: &str, options: &NumfmtOptions) -> Result<()> { + let mut output = String::new(); + for (n, (prefix, field)) in (1..).zip(WhitespaceSplitter { s: Some(s) }) { let field_selected = uucore::ranges::contain(&options.fields, n); if field_selected { let empty_prefix = prefix.is_empty(); - // print delimiter before second and subsequent fields + // add delimiter before second and subsequent fields let prefix = if n > 1 { - print!(" "); + output.push(' '); &prefix[1..] } else { prefix @@ -377,22 +399,24 @@ fn format_and_print_whitespace(s: &str, options: &NumfmtOptions) -> Result<()> { None }; - print!("{}", format_string(field, options, implicit_padding)?); + output.push_str(&format_string(field, options, implicit_padding)?); } else { // the -z option converts an initial \n into a space let prefix = if options.zero_terminated && prefix.starts_with('\n') { - print!(" "); + output.push(' '); &prefix[1..] } else { prefix }; - // print unselected field without conversion - print!("{prefix}{field}"); + // add unselected field without conversion + output.push_str(prefix); + output.push_str(field); } } let eol = if options.zero_terminated { '\0' } else { '\n' }; - print!("{eol}"); + output.push(eol); + print!("{output}"); Ok(()) } @@ -445,4 +469,123 @@ mod tests { assert_eq!(2, parse_implicit_precision("1.23K")); assert_eq!(3, parse_implicit_precision("1.234K")); } + + #[test] + fn test_parse_suffix_q_r_k() { + let result = parse_suffix("1Q"); + assert!(result.is_ok()); + let (number, suffix) = result.unwrap(); + assert_eq!(number, 1.0); + assert!(suffix.is_some()); + let (raw_suffix, with_i) = suffix.unwrap(); + assert_eq!(raw_suffix as i32, RawSuffix::Q as i32); + assert!(!with_i); + + let result = parse_suffix("2R"); + assert!(result.is_ok()); + let (number, suffix) = result.unwrap(); + assert_eq!(number, 2.0); + assert!(suffix.is_some()); + let (raw_suffix, with_i) = suffix.unwrap(); + assert_eq!(raw_suffix as i32, RawSuffix::R as i32); + assert!(!with_i); + + let result = parse_suffix("3k"); + assert!(result.is_ok()); + let (number, suffix) = result.unwrap(); + assert_eq!(number, 3.0); + assert!(suffix.is_some()); + let (raw_suffix, with_i) = suffix.unwrap(); + assert_eq!(raw_suffix as i32, RawSuffix::K as i32); + assert!(!with_i); + + let result = parse_suffix("4Qi"); + assert!(result.is_ok()); + let (number, suffix) = result.unwrap(); + assert_eq!(number, 4.0); + assert!(suffix.is_some()); + let (raw_suffix, with_i) = suffix.unwrap(); + assert_eq!(raw_suffix as i32, RawSuffix::Q as i32); + assert!(with_i); + + let result = parse_suffix("5Ri"); + assert!(result.is_ok()); + let (number, suffix) = result.unwrap(); + assert_eq!(number, 5.0); + assert!(suffix.is_some()); + let (raw_suffix, with_i) = suffix.unwrap(); + assert_eq!(raw_suffix as i32, RawSuffix::R as i32); + assert!(with_i); + } + + #[test] + fn test_parse_suffix_error_messages() { + let result = parse_suffix("foo"); + assert!(result.is_err()); + let error = result.unwrap_err(); + assert!(error.contains("numfmt-error-invalid-number") || error.contains("invalid number")); + assert!(!error.contains("invalid suffix")); + + let result = parse_suffix("World"); + assert!(result.is_err()); + let error = result.unwrap_err(); + assert!(error.contains("numfmt-error-invalid-number") || error.contains("invalid number")); + assert!(!error.contains("invalid suffix")); + + let result = parse_suffix("123i"); + assert!(result.is_err()); + let error = result.unwrap_err(); + assert!(error.contains("numfmt-error-invalid-suffix") || error.contains("invalid suffix")); + } + + #[test] + fn test_remove_suffix_q_r() { + use crate::units::Unit; + + let result = remove_suffix(1.0, Some((RawSuffix::Q, false)), &Unit::Si); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), 1e30); + + let result = remove_suffix(1.0, Some((RawSuffix::R, false)), &Unit::Si); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), 1e27); + + let result = remove_suffix(1.0, Some((RawSuffix::Q, true)), &Unit::Iec(true)); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), IEC_BASES[10]); + + let result = remove_suffix(1.0, Some((RawSuffix::R, true)), &Unit::Iec(true)); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), IEC_BASES[9]); + } + + #[test] + fn test_consider_suffix_q_r() { + use crate::options::RoundMethod; + use crate::units::Unit; + + let result = consider_suffix(1e27, &Unit::Si, RoundMethod::FromZero, 0); + assert!(result.is_ok()); + let (value, suffix) = result.unwrap(); + assert!(suffix.is_some()); + let (raw_suffix, _) = suffix.unwrap(); + assert_eq!(raw_suffix as i32, RawSuffix::R as i32); + assert_eq!(value, 1.0); + + let result = consider_suffix(1e30, &Unit::Si, RoundMethod::FromZero, 0); + assert!(result.is_ok()); + let (value, suffix) = result.unwrap(); + assert!(suffix.is_some()); + let (raw_suffix, _) = suffix.unwrap(); + assert_eq!(raw_suffix as i32, RawSuffix::Q as i32); + assert_eq!(value, 1.0); + + let result = consider_suffix(5e30, &Unit::Si, RoundMethod::FromZero, 0); + assert!(result.is_ok()); + let (value, suffix) = result.unwrap(); + assert!(suffix.is_some()); + let (raw_suffix, _) = suffix.unwrap(); + assert_eq!(raw_suffix as i32, RawSuffix::Q as i32); + assert_eq!(value, 5.0); + } } diff --git a/src/uu/numfmt/src/numfmt.rs b/src/uu/numfmt/src/numfmt.rs index f472b3593..abeaca256 100644 --- a/src/uu/numfmt/src/numfmt.rs +++ b/src/uu/numfmt/src/numfmt.rs @@ -460,9 +460,9 @@ mod tests { let result_display = format!("{result}"); assert_eq!( result_debug, - "FormattingError(\"numfmt-error-invalid-suffix\")" + "FormattingError(\"numfmt-error-invalid-number\")" ); - assert_eq!(result_display, "numfmt-error-invalid-suffix"); + assert_eq!(result_display, "numfmt-error-invalid-number"); assert_eq!(result.code(), 2); } diff --git a/src/uu/numfmt/src/units.rs b/src/uu/numfmt/src/units.rs index c52dee20c..bc5d480be 100644 --- a/src/uu/numfmt/src/units.rs +++ b/src/uu/numfmt/src/units.rs @@ -4,9 +4,9 @@ // file that was distributed with this source code. use std::fmt; -pub const SI_BASES: [f64; 10] = [1., 1e3, 1e6, 1e9, 1e12, 1e15, 1e18, 1e21, 1e24, 1e27]; +pub const SI_BASES: [f64; 11] = [1., 1e3, 1e6, 1e9, 1e12, 1e15, 1e18, 1e21, 1e24, 1e27, 1e30]; -pub const IEC_BASES: [f64; 10] = [ +pub const IEC_BASES: [f64; 11] = [ 1., 1_024., 1_048_576., @@ -17,6 +17,7 @@ pub const IEC_BASES: [f64; 10] = [ 1_180_591_620_717_411_303_424., 1_208_925_819_614_629_174_706_176., 1_237_940_039_285_380_274_899_124_224., + 1_267_650_600_228_229_401_496_703_205_376., ]; pub type WithI = bool; @@ -41,6 +42,8 @@ pub enum RawSuffix { E, Z, Y, + R, + Q, } pub type Suffix = (RawSuffix, WithI); @@ -60,6 +63,8 @@ impl fmt::Display for DisplayableSuffix { (RawSuffix::E, _) => write!(f, "E"), (RawSuffix::Z, _) => write!(f, "Z"), (RawSuffix::Y, _) => write!(f, "Y"), + (RawSuffix::R, _) => write!(f, "R"), + (RawSuffix::Q, _) => write!(f, "Q"), } .and_then(|()| match with_i { true => write!(f, "i"), diff --git a/tests/by-util/test_numfmt.rs b/tests/by-util/test_numfmt.rs index 673073694..d947833f7 100644 --- a/tests/by-util/test_numfmt.rs +++ b/tests/by-util/test_numfmt.rs @@ -241,8 +241,7 @@ fn test_should_report_invalid_empty_number_on_blank_stdin() { #[test] fn test_suffixes() { - // TODO add support for ronna (R) and quetta (Q) - let valid_suffixes = ['K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y' /*'R' , 'Q'*/]; + let valid_suffixes = ['K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y', 'R', 'Q', 'k']; for c in ('A'..='Z').chain('a'..='z') { let args = ["--from=si", "--to=si", &format!("1{c}")]; @@ -264,12 +263,12 @@ fn test_suffixes() { #[test] fn test_should_report_invalid_suffix_on_nan() { - // GNU numfmt reports this one as “invalid number” + // GNU numfmt reports this one as "invalid number" new_ucmd!() .args(&["--from=auto"]) .pipe_in("NaN") .fails() - .stderr_is("numfmt: invalid suffix in input: 'NaN'\n"); + .stderr_is("numfmt: invalid number: 'NaN'\n"); } #[test] @@ -700,7 +699,7 @@ fn test_invalid_stdin_number_with_warn_returns_status_0() { .pipe_in("4Q") .succeeds() .stdout_is("4Q\n") - .stderr_is("numfmt: invalid suffix in input: '4Q'\n"); + .stderr_is("numfmt: rejecting suffix in input: '4Q' (consider using --from)\n"); } #[test] @@ -718,7 +717,7 @@ fn test_invalid_stdin_number_with_abort_returns_status_2() { .args(&["--invalid=abort"]) .pipe_in("4Q") .fails_with_code(2) - .stderr_only("numfmt: invalid suffix in input: '4Q'\n"); + .stderr_only("numfmt: rejecting suffix in input: '4Q' (consider using --from)\n"); } #[test] @@ -728,7 +727,7 @@ fn test_invalid_stdin_number_with_fail_returns_status_2() { .pipe_in("4Q") .fails_with_code(2) .stdout_is("4Q\n") - .stderr_is("numfmt: invalid suffix in input: '4Q'\n"); + .stderr_is("numfmt: rejecting suffix in input: '4Q' (consider using --from)\n"); } #[test] @@ -737,7 +736,7 @@ fn test_invalid_arg_number_with_warn_returns_status_0() { .args(&["--invalid=warn", "4Q"]) .succeeds() .stdout_is("4Q\n") - .stderr_is("numfmt: invalid suffix in input: '4Q'\n"); + .stderr_is("numfmt: rejecting suffix in input: '4Q' (consider using --from)\n"); } #[test] @@ -753,7 +752,7 @@ fn test_invalid_arg_number_with_abort_returns_status_2() { new_ucmd!() .args(&["--invalid=abort", "4Q"]) .fails_with_code(2) - .stderr_only("numfmt: invalid suffix in input: '4Q'\n"); + .stderr_only("numfmt: rejecting suffix in input: '4Q' (consider using --from)\n"); } #[test] @@ -762,7 +761,7 @@ fn test_invalid_arg_number_with_fail_returns_status_2() { .args(&["--invalid=fail", "4Q"]) .fails_with_code(2) .stdout_is("4Q\n") - .stderr_is("numfmt: invalid suffix in input: '4Q'\n"); + .stderr_is("numfmt: rejecting suffix in input: '4Q' (consider using --from)\n"); } #[test] From 5202ac137d06c8f187f55961ad4994f7fe48ce0b Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Sat, 15 Nov 2025 18:05:04 +0900 Subject: [PATCH 050/182] Merge pull request #8730 from oech3/profile GNUmakefile: Use any profile from make install --- .github/workflows/CICD.yml | 7 +++++-- GNUmakefile | 12 ++++++------ README.md | 12 +++++++++--- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index dd09756b0..37caac9fc 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -303,11 +303,14 @@ jobs: run: make nextest PROFILE=ci CARGOFLAGS="--hide-progress-bar" env: RUST_BACKTRACE: "1" - - name: "`make install COMPLETIONS=n MANPAGES=n LOCALES=n`" + + - name: "`make install PROFILE=release-fast COMPLETIONS=n MANPAGES=n LOCALES=n`" shell: bash run: | set -x - DESTDIR=/tmp/ make PROFILE=release COMPLETIONS=n MANPAGES=n LOCALES=n install + DESTDIR=/tmp/ make PROFILE=release-fast COMPLETIONS=n MANPAGES=n LOCALES=n install + # Check that utils are built with given profile + ./target/release-fast/true # Check that the utils are present test -f /tmp/usr/local/bin/tty # Check that the manpage is not present diff --git a/GNUmakefile b/GNUmakefile index f262c50ea..f200641d4 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -1,15 +1,15 @@ # spell-checker:ignore (misc) testsuite runtest findstring (targets) busytest toybox distclean pkgs nextest ; (vars/env) BINDIR BUILDDIR CARGOFLAGS DESTDIR DOCSDIR INSTALLDIR INSTALLEES MULTICALL DATAROOTDIR TESTDIR manpages # Config options +ifneq (,$(filter install, $(MAKECMDGOALS))) + PROFILE?=release +endif PROFILE ?= debug MULTICALL ?= n COMPLETIONS ?= y MANPAGES ?= y LOCALES ?= y INSTALL ?= install -ifneq (,$(filter install, $(MAKECMDGOALS))) -override PROFILE:=release -endif # Needed for the foreach loops to split each loop into a separate command define newline @@ -17,9 +17,9 @@ define newline endef -PROFILE_CMD := -ifeq ($(PROFILE),release) - PROFILE_CMD = --release +PROFILE_CMD := --profile=${PROFILE} +ifeq ($(PROFILE),debug) + PROFILE_CMD = endif # Binaries diff --git a/README.md b/README.md index e8072a095..64785e8df 100644 --- a/README.md +++ b/README.md @@ -152,16 +152,16 @@ cargo build -p uu_base32 -p uu_cat -p uu_echo -p uu_rm Building using `make` is a simple process as well. -To simply build all available utilities: +To simply build all available utilities (with debug profile): ```shell make ``` -In release mode: +In release-fast mode: ```shell -make PROFILE=release +make PROFILE=release-fast ``` To build all but a few of the available utilities: @@ -201,6 +201,12 @@ To install all available utilities: make install ``` +To install all utilities with all possible optimizations: + +```shell +make PROFILE=release-fast install +``` + To install using `sudo` switch `-E` must be used: ```shell From 8fc9d28001aab4a5546d49a66db734b7b86b3add Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Sat, 15 Nov 2025 21:48:23 +0900 Subject: [PATCH 051/182] Merge pull request #9277 from oech3/patch-2 CICD.yml: Avoid no space left --- .github/workflows/CICD.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index 37caac9fc..5486650e4 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -1202,6 +1202,7 @@ jobs: - name: build and test all features individually shell: bash run: | + command -v sudo && sudo rm -rf /usr/share/dotnet # avoid no space left CARGO_FEATURES_OPTION='--features=${{ matrix.job.features }}' ; for f in $(util/show-utils.sh ${CARGO_FEATURES_OPTION}) do From 5be296423068e9838a450257edced2b1d54e37f0 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Sat, 15 Nov 2025 22:10:05 +0900 Subject: [PATCH 052/182] Fix build failure without libselinux --- .github/workflows/CICD.yml | 5 +---- GNUmakefile | 12 ++++-------- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index 5486650e4..5cd9526e8 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -273,10 +273,7 @@ jobs: target: aarch64-unknown-linux-gnu - uses: taiki-e/install-action@nextest - uses: Swatinem/rust-cache@v2 - - name: Install/setup prerequisites - shell: bash - run: | - sudo apt-get -y update ; sudo apt-get -y install libselinux1-dev + # Test build on the system missing libselinux (don't install libselinux1-dev at here) - name: Run sccache-cache uses: mozilla-actions/sccache-action@v0.0.9 - name: "`make build`" diff --git a/GNUmakefile b/GNUmakefile index f200641d4..aff779f3e 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -195,15 +195,11 @@ HASHSUM_PROGS := \ $(info Detected OS = $(OS)) -# Build the SELinux programs only on Linux -ifeq ($(filter $(OS),Linux),) - SELINUX_PROGS := -endif - ifneq ($(OS),Windows_NT) - PROGS := $(PROGS) $(UNIX_PROGS) -# Build the selinux command even if not on the system - PROGS := $(PROGS) $(SELINUX_PROGS) + PROGS += $(UNIX_PROGS) +endif +ifeq ($(SELINUX_ENABLED),1) + PROGS += $(SELINUX_PROGS) endif UTILS ?= $(filter-out $(SKIP_UTILS),$(PROGS)) From 87b116eccb49520b7716bd51ca8d6c132f8a6e84 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 15 Nov 2025 13:41:06 +0000 Subject: [PATCH 053/182] chore(deps): update rust crate crc-fast to v1.8.0 --- Cargo.lock | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f2f8412cc..8c2b80f37 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -744,13 +744,14 @@ checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "crc-fast" -version = "1.7.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffde0dda52b6befc15f7d1c573d2935cda15dc81bd546ef76d8679b2bf85a300" +checksum = "a2f7c8d397a6353ef0c1d6217ab91b3ddb5431daf57fd013f506b967dcf44458" dependencies = [ "crc", "digest", "rustversion", + "spin", ] [[package]] @@ -2807,6 +2808,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "spin" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" + [[package]] name = "stable_deref_trait" version = "1.2.0" From c1734e039c6abafbdad79c76ae1278df53222dac Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Sun, 16 Nov 2025 03:45:14 +0900 Subject: [PATCH 054/182] GNUmakefile: Add missing PROFILE_CMD --- GNUmakefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GNUmakefile b/GNUmakefile index f200641d4..cb6dc116b 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -337,7 +337,7 @@ test: ${CARGO} test ${CARGOFLAGS} --features "$(TESTS) $(TEST_SPEC_FEATURE)" $(PROFILE_CMD) --no-default-features $(TEST_NO_FAIL_FAST) nextest: - ${CARGO} nextest run ${CARGOFLAGS} --features "$(TESTS) $(TEST_SPEC_FEATURE)" --no-default-features $(TEST_NO_FAIL_FAST) + ${CARGO} nextest run ${CARGOFLAGS} --features "$(TESTS) $(TEST_SPEC_FEATURE)" $(PROFILE_CMD) --no-default-features $(TEST_NO_FAIL_FAST) test_toybox: -(cd $(TOYBOX_SRC)/ && make tests) From 09e1aa18ab8cab87018ed7dfb2d961eb3a5abbdb Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Thu, 13 Nov 2025 23:10:37 +0100 Subject: [PATCH 055/182] sort: make compression program failures non-fatal, warn and fallback to plain files Should fix tests/sort/sort-compress.sh --- src/uu/sort/locales/en-US.ftl | 2 +- src/uu/sort/locales/fr-FR.ftl | 2 +- src/uu/sort/src/ext_sort.rs | 35 +++++++++++++++++++++++--- src/uu/sort/src/merge.rs | 6 +++-- src/uu/sort/src/sort.rs | 4 +-- tests/by-util/test_sort.rs | 47 +++++++++++++++++------------------ 6 files changed, 62 insertions(+), 34 deletions(-) diff --git a/src/uu/sort/locales/en-US.ftl b/src/uu/sort/locales/en-US.ftl index a13e932af..21042721a 100644 --- a/src/uu/sort/locales/en-US.ftl +++ b/src/uu/sort/locales/en-US.ftl @@ -15,7 +15,7 @@ sort-open-failed = open failed: {$path}: {$error} sort-parse-key-error = failed to parse key {$key}: {$msg} sort-cannot-read = cannot read: {$path}: {$error} sort-open-tmp-file-failed = failed to open temporary file: {$error} -sort-compress-prog-execution-failed = couldn't execute compress program: errno {$code} +sort-compress-prog-execution-failed = could not run compress program '{$prog}': {$error} sort-compress-prog-terminated-abnormally = {$prog} terminated abnormally sort-cannot-create-tmp-file = cannot create temporary file in '{$path}': sort-file-operands-combined = extra operand '{$file}' diff --git a/src/uu/sort/locales/fr-FR.ftl b/src/uu/sort/locales/fr-FR.ftl index f434607d5..611613c51 100644 --- a/src/uu/sort/locales/fr-FR.ftl +++ b/src/uu/sort/locales/fr-FR.ftl @@ -15,7 +15,7 @@ sort-open-failed = échec d'ouverture : {$path} : {$error} sort-parse-key-error = échec d'analyse de la clé {$key} : {$msg} sort-cannot-read = impossible de lire : {$path} : {$error} sort-open-tmp-file-failed = échec d'ouverture du fichier temporaire : {$error} -sort-compress-prog-execution-failed = impossible d'exécuter le programme de compression : errno {$code} +sort-compress-prog-execution-failed = impossible d'exécuter le programme de compression '{$prog}' : {$error} sort-compress-prog-terminated-abnormally = {$prog} s'est terminé anormalement sort-cannot-create-tmp-file = impossible de créer un fichier temporaire dans '{$path}' : sort-file-operands-combined = opérande supplémentaire '{$file}' diff --git a/src/uu/sort/src/ext_sort.rs b/src/uu/sort/src/ext_sort.rs index ddbc278d7..d61f7d200 100644 --- a/src/uu/sort/src/ext_sort.rs +++ b/src/uu/sort/src/ext_sort.rs @@ -20,7 +20,7 @@ use std::{ }; use itertools::Itertools; -use uucore::error::UResult; +use uucore::error::{UResult, strip_errno}; use crate::Output; use crate::chunks::RecycledChunk; @@ -52,10 +52,37 @@ pub fn ext_sort( let settings = settings.clone(); move || sorter(&recycled_receiver, &sorted_sender, &settings) }); - if settings.compress_prog.is_some() { + + // Test if compression program exists and works, disable if not + let mut effective_settings = settings.clone(); + if let Some(ref prog) = settings.compress_prog { + // Test the compression program by trying to spawn it + match std::process::Command::new(prog) + .stdin(std::process::Stdio::null()) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .spawn() + { + Ok(mut child) => { + // Kill the test process immediately + let _ = child.kill(); + } + Err(err) => { + // Print the error and disable compression + eprintln!( + "sort: could not run compress program '{}': {}", + prog, + strip_errno(&err) + ); + effective_settings.compress_prog = None; + } + } + } + + if effective_settings.compress_prog.is_some() { reader_writer::<_, WriteableCompressedTmpFile>( files, - settings, + &effective_settings, &sorted_receiver, recycled_sender, output, @@ -64,7 +91,7 @@ pub fn ext_sort( } else { reader_writer::<_, WriteablePlainTmpFile>( files, - settings, + &effective_settings, &sorted_receiver, recycled_sender, output, diff --git a/src/uu/sort/src/merge.rs b/src/uu/sort/src/merge.rs index 1e538c6d9..ea212f62f 100644 --- a/src/uu/sort/src/merge.rs +++ b/src/uu/sort/src/merge.rs @@ -488,7 +488,8 @@ impl WriteableTmpFile for WriteableCompressedTmpFile { let mut child = command .spawn() .map_err(|err| SortError::CompressProgExecutionFailed { - code: err.raw_os_error().unwrap(), + prog: compress_prog.to_owned(), + error: err, })?; let child_stdin = child.stdin.take().unwrap(); Ok(Self { @@ -522,7 +523,8 @@ impl ClosedTmpFile for ClosedCompressedTmpFile { let mut child = command .spawn() .map_err(|err| SortError::CompressProgExecutionFailed { - code: err.raw_os_error().unwrap(), + prog: self.compress_prog.clone(), + error: err, })?; let child_stdout = child.stdout.take().unwrap(); Ok(CompressedTmpMergeInput { diff --git a/src/uu/sort/src/sort.rs b/src/uu/sort/src/sort.rs index 22c96a436..ec9ab5b93 100644 --- a/src/uu/sort/src/sort.rs +++ b/src/uu/sort/src/sort.rs @@ -152,8 +152,8 @@ pub enum SortError { #[error("{}", translate!("sort-open-tmp-file-failed", "error" => strip_errno(.error)))] OpenTmpFileFailed { error: std::io::Error }, - #[error("{}", translate!("sort-compress-prog-execution-failed", "code" => .code))] - CompressProgExecutionFailed { code: i32 }, + #[error("{}", translate!("sort-compress-prog-execution-failed", "prog" => .prog, "error" => strip_errno(.error)))] + CompressProgExecutionFailed { prog: String, error: std::io::Error }, #[error("{}", translate!("sort-compress-prog-terminated-abnormally", "prog" => .prog.quote()))] CompressProgTerminatedAbnormally { prog: String }, diff --git a/tests/by-util/test_sort.rs b/tests/by-util/test_sort.rs index 3a4cc1a86..8bce9d69c 100644 --- a/tests/by-util/test_sort.rs +++ b/tests/by-util/test_sort.rs @@ -1002,32 +1002,31 @@ fn test_compress_merge() { #[test] #[cfg(not(target_os = "android"))] fn test_compress_fail() { + let result = new_ucmd!() + .args(&[ + "ext_sort.txt", + "-n", + "--compress-program", + "nonexistent-program", + "-S", + "10", + ]) + .succeeds(); + #[cfg(not(windows))] - new_ucmd!() - .args(&[ - "ext_sort.txt", - "-n", - "--compress-program", - "nonexistent-program", - "-S", - "10", - ]) - .fails() - .stderr_only("sort: couldn't execute compress program: errno 2\n"); - // With coverage, it fails with a different error: - // "thread 'main' panicked at 'called `Option::unwrap()` on ... - // So, don't check the output + result.stderr_contains( + "sort: could not run compress program 'nonexistent-program': No such file or directory", + ); + #[cfg(windows)] - new_ucmd!() - .args(&[ - "ext_sort.txt", - "-n", - "--compress-program", - "nonexistent-program", - "-S", - "10", - ]) - .fails(); + result.stderr_contains("could not run compress program"); + + // Check that it still produces correct sorted output to stdout + let expected = new_ucmd!() + .args(&["ext_sort.txt", "-n"]) + .succeeds() + .stdout_move_str(); + assert_eq!(result.stdout_str(), expected); } #[test] From ecc7f14e7e87e2acb195ebbeca92148eee7d2546 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sat, 15 Nov 2025 21:45:22 +0100 Subject: [PATCH 056/182] move the factor divan bench into the actual directory --- Cargo.lock | 18 +------- Cargo.toml | 1 - src/uu/factor/Cargo.toml | 1 + src/uu/factor/benches/factor_bench.rs | 56 ++++++++++++++++++++++++ tests/benches/factor/Cargo.toml | 20 --------- tests/benches/factor/benches/table.rs | 62 --------------------------- 6 files changed, 58 insertions(+), 100 deletions(-) delete mode 100644 tests/benches/factor/Cargo.toml delete mode 100644 tests/benches/factor/benches/table.rs diff --git a/Cargo.lock b/Cargo.lock index 406b3be90..199569ec9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -115,12 +115,6 @@ dependencies = [ "derive_arbitrary", ] -[[package]] -name = "array-init" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d62b7694a562cdf5a74227903507c56ab2cc8bdd1f781ed5cb4cf9c9f810bfc" - [[package]] name = "arrayref" version = "0.3.9" @@ -3335,18 +3329,8 @@ dependencies = [ "num-bigint", "num-prime", "num-traits", - "uucore", -] - -[[package]] -name = "uu_factor_benches" -version = "0.0.0" -dependencies = [ - "array-init", - "codspeed-divan-compat", - "num-prime", "rand 0.9.2", - "rand_chacha 0.9.0", + "uucore", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 499eb8741..5d0da6da1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -280,7 +280,6 @@ members = [ "src/uu/stdbuf/src/libstdbuf", "src/uucore", "src/uucore_procs", - "tests/benches/factor", "tests/uutests", # "fuzz", # TODO ] diff --git a/src/uu/factor/Cargo.toml b/src/uu/factor/Cargo.toml index 15d09f7a0..ef672bf93 100644 --- a/src/uu/factor/Cargo.toml +++ b/src/uu/factor/Cargo.toml @@ -31,6 +31,7 @@ path = "src/main.rs" [dev-dependencies] divan = { workspace = true } +rand = { workspace = true } uucore = { workspace = true, features = ["benchmark"] } [lib] diff --git a/src/uu/factor/benches/factor_bench.rs b/src/uu/factor/benches/factor_bench.rs index 89498e0ae..0346f9787 100644 --- a/src/uu/factor/benches/factor_bench.rs +++ b/src/uu/factor/benches/factor_bench.rs @@ -3,6 +3,8 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +// spell-checker:ignore funcs + use divan::{Bencher, black_box}; use uu_factor::uumain; use uucore::benchmark::run_util_function; @@ -55,6 +57,60 @@ fn factor_multiple_big_uint(bencher: Bencher) { } */ +#[divan::bench()] +fn factor_table(bencher: Bencher) { + #[cfg(target_os = "linux")] + check_personality(); + + const INPUT_SIZE: usize = 128; + + let inputs = { + // Deterministic RNG; use an explicitly-named RNG to guarantee stability + use rand::{RngCore, SeedableRng}; + const SEED: u64 = 0xdead_bebe_ea75_cafe; // spell-checker:disable-line + let mut rng = rand::rngs::StdRng::seed_from_u64(SEED); + + std::iter::repeat_with(move || { + let mut array = [0u64; INPUT_SIZE]; + for item in &mut array { + *item = rng.next_u64(); + } + array + }) + .take(10) + .collect::>() + }; + + bencher.bench(|| { + for a in &inputs { + for n in a { + divan::black_box(num_prime::nt_funcs::factors(*n, None)); + } + } + }); +} + +#[cfg(target_os = "linux")] +fn check_personality() { + use std::fs; + const ADDR_NO_RANDOMIZE: u64 = 0x0040000; + const PERSONALITY_PATH: &str = "/proc/self/personality"; + + let p_string = fs::read_to_string(PERSONALITY_PATH) + .unwrap_or_else(|_| panic!("Couldn't read '{PERSONALITY_PATH}'")) + .strip_suffix('\n') + .unwrap() + .to_owned(); + + let personality = u64::from_str_radix(&p_string, 16) + .unwrap_or_else(|_| panic!("Expected a hex value for personality, got '{p_string:?}'")); + if personality & ADDR_NO_RANDOMIZE == 0 { + eprintln!( + "WARNING: Benchmarking with ASLR enabled (personality is {personality:x}), results might not be reproducible." + ); + } +} + fn main() { divan::main(); } diff --git a/tests/benches/factor/Cargo.toml b/tests/benches/factor/Cargo.toml deleted file mode 100644 index 02e59925c..000000000 --- a/tests/benches/factor/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "uu_factor_benches" -version = "0.0.0" -authors = ["nicoo "] -description = "Benchmarks for the uu_factor integer factorization tool" -edition.workspace = true -homepage.workspace = true -license.workspace = true -publish = false - -[dev-dependencies] -array-init = "2.0.0" -divan = { workspace = true } -rand = "0.9.1" -rand_chacha = "0.9.0" -num-prime = "0.4.4" - -[[bench]] -name = "table" -harness = false diff --git a/tests/benches/factor/benches/table.rs b/tests/benches/factor/benches/table.rs deleted file mode 100644 index 4d89282fa..000000000 --- a/tests/benches/factor/benches/table.rs +++ /dev/null @@ -1,62 +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. - -// spell-checker:ignore funcs - -use array_init::array_init; -use divan::Bencher; - -fn main() { - divan::main(); -} - -#[divan::bench()] -fn factor_table(bencher: Bencher) { - #[cfg(target_os = "linux")] - check_personality(); - - const INPUT_SIZE: usize = 128; - - let inputs = { - // Deterministic RNG; use an explicitly-named RNG to guarantee stability - use rand::{RngCore, SeedableRng}; - use rand_chacha::ChaCha8Rng; - const SEED: u64 = 0xdead_bebe_ea75_cafe; // spell-checker:disable-line - let mut rng = ChaCha8Rng::seed_from_u64(SEED); - - std::iter::repeat_with(move || array_init::<_, _, INPUT_SIZE>(|_| rng.next_u64())) - .take(10) - .collect::>() - }; - - bencher.bench(|| { - for a in &inputs { - for n in a { - divan::black_box(num_prime::nt_funcs::factors(*n, None)); - } - } - }); -} - -#[cfg(target_os = "linux")] -fn check_personality() { - use std::fs; - const ADDR_NO_RANDOMIZE: u64 = 0x0040000; - const PERSONALITY_PATH: &str = "/proc/self/personality"; - - let p_string = fs::read_to_string(PERSONALITY_PATH) - .unwrap_or_else(|_| panic!("Couldn't read '{PERSONALITY_PATH}'")) - .strip_suffix('\n') - .unwrap() - .to_owned(); - - let personality = u64::from_str_radix(&p_string, 16) - .unwrap_or_else(|_| panic!("Expected a hex value for personality, got '{p_string:?}'")); - if personality & ADDR_NO_RANDOMIZE == 0 { - eprintln!( - "WARNING: Benchmarking with ASLR enabled (personality is {personality:x}), results might not be reproducible." - ); - } -} From 0500ee3d76385c223d1c9188cf547d91a3f00f31 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sat, 15 Nov 2025 22:22:31 +0100 Subject: [PATCH 057/182] =?UTF-8?q?build-gnu.sh:=20fix=20the=20error=20on?= =?UTF-8?q?=20line=20110=20util/build-gnu.sh:=20ligne=20110=20:=20[:=20=3D?= =?UTF-8?q?=20:=20op=C3=A9rateur=20unaire=20attendu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- util/build-gnu.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/util/build-gnu.sh b/util/build-gnu.sh index cb23f0ad0..f3596c411 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -107,7 +107,7 @@ cd - # Pass the feature flags to make, which will pass them to cargo "${MAKE}" PROFILE="${UU_MAKE_PROFILE}" CARGOFLAGS="${CARGO_FEATURE_FLAGS}" # min test for SELinux -[ ${SELINUX_ENABLED} = 1 ] && touch g && "${UU_MAKE_PROFILE}"/stat -c%C g && rm g +[ "${SELINUX_ENABLED}" = 1 ] && touch g && "${UU_MAKE_PROFILE}"/stat -c%C g && rm g cp "${UU_BUILD_DIR}/install" "${UU_BUILD_DIR}/ginstall" # The GNU tests rename this script before running, to avoid confusion with the make target # Create *sum binaries @@ -141,7 +141,7 @@ else sed -i "s/^[[:blank:]]*PATH=.*/ PATH='${UU_BUILD_DIR//\//\\/}\$(PATH_SEPARATOR)'\"\$\$PATH\" \\\/" tests/local.mk ./bootstrap --skip-po ./configure --quiet --disable-gcc-warnings --disable-nls --disable-dependency-tracking --disable-bold-man-page-references \ - "$([ ${SELINUX_ENABLED} = 1 ] && echo --with-selinux || echo --without-selinux)" + "$([ "${SELINUX_ENABLED}" = 1 ] && echo --with-selinux || echo --without-selinux)" #Add timeout to to protect against hangs sed -i 's|^"\$@|'"${SYSTEM_TIMEOUT}"' 600 "\$@|' build-aux/test-driver sed -i 's| tr | /usr/bin/tr |' tests/init.sh From 2ab03d97353a047fec66983d37020c3d778f4031 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 16 Nov 2025 13:05:58 +0000 Subject: [PATCH 058/182] Update vmactions/freebsd-vm action to v1.2.7 --- .github/workflows/freebsd.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/freebsd.yml b/.github/workflows/freebsd.yml index 89a9a6f18..4a123789f 100644 --- a/.github/workflows/freebsd.yml +++ b/.github/workflows/freebsd.yml @@ -43,7 +43,7 @@ jobs: with: disable_annotations: true - name: Prepare, build and test - uses: vmactions/freebsd-vm@v1.2.6 + uses: vmactions/freebsd-vm@v1.2.7 with: usesh: true sync: rsync @@ -139,7 +139,7 @@ jobs: with: disable_annotations: true - name: Prepare, build and test - uses: vmactions/freebsd-vm@v1.2.6 + uses: vmactions/freebsd-vm@v1.2.7 with: usesh: true sync: rsync From 6e99389d0f3348839e064a6338cea9ebc4f2049d Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Sun, 16 Nov 2025 22:35:05 +0900 Subject: [PATCH 059/182] Revert "ci: Mark runcon-no-reorder as SELinux required" (#9291) --- util/gnu-patches/runcon-no-reorder.patch | 14 -------------- util/gnu-patches/series | 1 - 2 files changed, 15 deletions(-) delete mode 100644 util/gnu-patches/runcon-no-reorder.patch diff --git a/util/gnu-patches/runcon-no-reorder.patch b/util/gnu-patches/runcon-no-reorder.patch deleted file mode 100644 index 833e37dca..000000000 --- a/util/gnu-patches/runcon-no-reorder.patch +++ /dev/null @@ -1,14 +0,0 @@ ---git a/tests/runcon/runcon-no-reorder.sh b/tests/runcon/runcon-no-reorder.sh -index 2027555..956c51e 100644 ---- a/tests/runcon/runcon-no-reorder.sh -+++ b/tests/runcon/runcon-no-reorder.sh -@@ -16,6 +16,9 @@ - # You should have received a copy of the GNU General Public License - # along with this program. If not, see . - -+# We don't have runcon buildable without libselinux. -+_require_selinux_ -+ - . "${srcdir=.}/tests/init.sh"; path_prepend_ ./src - print_ver_ runcon - diff --git a/util/gnu-patches/series b/util/gnu-patches/series index e47d52242..5fb1398cd 100644 --- a/util/gnu-patches/series +++ b/util/gnu-patches/series @@ -11,4 +11,3 @@ tests_tsort.patch tests_du_move_dir_while_traversing.patch test_mkdir_restorecon.patch error_msg_uniq.diff -runcon-no-reorder.patch From 1c8bcab5d5d88a879cbb9d385f3ff121d41a9b8f Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Mon, 17 Nov 2025 08:22:20 +0100 Subject: [PATCH 060/182] Bump markdownlint_cli2_action from v20 to v21 --- .github/workflows/CICD.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index 5cd9526e8..a1c88a316 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -149,7 +149,7 @@ jobs: shell: bash run: | RUSTDOCFLAGS="-Dwarnings" cargo doc ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} --no-deps --workspace --document-private-items - - uses: DavidAnson/markdownlint-cli2-action@v20 + - uses: DavidAnson/markdownlint-cli2-action@v21 with: fix: "true" globs: | From e82cc75433b958ae846595a0c5f0037c2b29efa8 Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Mon, 17 Nov 2025 09:07:04 +0100 Subject: [PATCH 061/182] docs: fix warnings from markdownlint --- docs/src/platforms.md | 10 +++++----- src/uu/join/BENCHMARKING.md | 16 ++++++++-------- src/uu/wc/BENCHMARKING.md | 20 ++++++++++---------- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/docs/src/platforms.md b/docs/src/platforms.md index b84516e3f..12f08d709 100644 --- a/docs/src/platforms.md +++ b/docs/src/platforms.md @@ -1,6 +1,6 @@ # Platform support - + uutils aims to be as "universal" as possible, meaning that we try to support many platforms. However, it is infeasible for us to guarantee that every @@ -19,13 +19,13 @@ platform support, with different guarantees. We support two tiers of platforms: The platforms in tier 1 and the platforms that we test in CI are listed below. -| Operating system | Tested targets | -| ---------------- | -------------- | +| Operating system | Tested targets | +| ---------------- | ------------------------ | | **Linux** | `x86_64-unknown-linux-gnu`
`x86_64-unknown-linux-musl`
`arm-unknown-linux-gnueabihf`
`i686-unknown-linux-gnu`
`aarch64-unknown-linux-gnu` | -| **macOS** | `x86_64-apple-darwin` | +| **macOS** | `x86_64-apple-darwin` | | **Windows** | `i686-pc-windows-msvc`
`x86_64-pc-windows-gnu`
`x86_64-pc-windows-msvc` | | **FreeBSD** | `x86_64-unknown-freebsd` | -| **Android** | `i686-linux-android` | +| **Android** | `i686-linux-android` | The platforms in tier 2 are more vague, but include: diff --git a/src/uu/join/BENCHMARKING.md b/src/uu/join/BENCHMARKING.md index 988259aa7..1698a2ff8 100644 --- a/src/uu/join/BENCHMARKING.md +++ b/src/uu/join/BENCHMARKING.md @@ -7,14 +7,14 @@ The amount of time spent in which part of the code can vary depending on the files being joined and the flags used. A benchmark with `-j` and `-i` shows the following time: -| Function/Method | Fraction of Samples | Why? | -| ---------------- | ------------------- | ---- | -| `Line::new` | 27% | Linear search for field separators, plus some vector operations. | -| `read_until` | 22% | Mostly libc reading file contents, with a few vector operations to represent them. | -| `Input::compare` | 20% | ~2/3 making the keys lowercase, ~1/3 comparing them. | -| `print_fields` | 11% | Writing to and flushing the buffer. | -| Other | 20% | | -| libc | 25% | I/O and memory allocation. | +| Function/Method | Fraction of Samples | Why? | +| ---------------- | ------------------- | ---------------------------------------------------------------------------------- | +| `Line::new` | 27% | Linear search for field separators, plus some vector operations. | +| `read_until` | 22% | Mostly libc reading file contents, with a few vector operations to represent them. | +| `Input::compare` | 20% | ~2/3 making the keys lowercase, ~1/3 comparing them. | +| `print_fields` | 11% | Writing to and flushing the buffer. | +| Other | 20% | | +| libc | 25% | I/O and memory allocation. | More detailed profiles can be obtained via [flame graphs](https://github.com/flamegraph-rs/flamegraph): diff --git a/src/uu/wc/BENCHMARKING.md b/src/uu/wc/BENCHMARKING.md index 60f9139da..d65c17d3f 100644 --- a/src/uu/wc/BENCHMARKING.md +++ b/src/uu/wc/BENCHMARKING.md @@ -29,7 +29,7 @@ suitable, and that if a file is given as its input directly (as in ### Counting lines and UTF-8 characters If the flags set are a subset of `-clm` then the input doesn't have to be decoded. The -input is read in chunks and the `bytecount` crate is used to count the newlines (`-l` flag) +input is read in chunks and the `bytecount` crate is used to count the newlines (`-l` flag) and/or UTF-8 characters (`-m` flag). It's useful to vary the line length in the input. GNU wc seems particularly @@ -83,16 +83,16 @@ performance. For example, `hyperfine 'wc somefile' 'uuwc somefile'`. If you want to get fancy and exhaustive, generate a table: -| | moby64.txt | odyssey256.txt | 25Mshortlines | /usr/bin/docker | -|------------------------|--------------|------------------|-----------------|-------------------| -| `wc ` | 1.3965 | 1.6182 | 5.2967 | 2.2294 | -| `wc -c ` | 0.8134 | 1.2774 | 0.7732 | 0.9106 | +| | moby64.txt | odyssey256.txt | 25Mshortlines | /usr/bin/docker | +|-------------------------|--------------|------------------|-----------------|-------------------| +| `wc ` | 1.3965 | 1.6182 | 5.2967 | 2.2294 | +| `wc -c ` | 0.8134 | 1.2774 | 0.7732 | 0.9106 | | `uucat \| wc -c` | 2.7760 | 2.5565 | 2.3769 | 2.3982 | -| `wc -l ` | 1.1441 | 1.2854 | 2.9681 | 1.1493 | -| `wc -L ` | 2.1087 | 1.2551 | 5.4577 | 2.1490 | -| `wc -m ` | 2.7272 | 2.1704 | 7.3371 | 3.4347 | -| `wc -w ` | 1.9007 | 1.5206 | 4.7851 | 2.8529 | -| `wc -lwcmL ` | 1.1687 | 0.9169 | 4.4092 | 2.0663 | +| `wc -l ` | 1.1441 | 1.2854 | 2.9681 | 1.1493 | +| `wc -L ` | 2.1087 | 1.2551 | 5.4577 | 2.1490 | +| `wc -m ` | 2.7272 | 2.1704 | 7.3371 | 3.4347 | +| `wc -w ` | 1.9007 | 1.5206 | 4.7851 | 2.8529 | +| `wc -lwcmL ` | 1.1687 | 0.9169 | 4.4092 | 2.0663 | Beware that: From c17598370c21e1f07d692e8667700303f696048f Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Mon, 17 Nov 2025 10:11:40 +0100 Subject: [PATCH 062/182] Remove high variance benchmark functions --- src/uu/factor/benches/factor_bench.rs | 89 --------------------------- 1 file changed, 89 deletions(-) diff --git a/src/uu/factor/benches/factor_bench.rs b/src/uu/factor/benches/factor_bench.rs index 0346f9787..952ea09a6 100644 --- a/src/uu/factor/benches/factor_bench.rs +++ b/src/uu/factor/benches/factor_bench.rs @@ -22,95 +22,6 @@ fn factor_multiple_u64s(bencher: Bencher, start_num: u64) { }); } -/* Too much variance -/// Benchmark multiple u128 digits -#[divan::bench(args = [(18446744073709551616)])] -fn factor_multiple_u128s(bencher: Bencher, start_num: u128) { - bencher - .with_inputs(|| { - // this is a range of 1000 different u128 integers - (start_num, start_num + 1000) - }) - .bench_values(|(start_u128, end_u128)| { - for u128_digit in start_u128..=end_u128 { - black_box(run_util_function(uumain, &[&u128_digit.to_string()])); - } - }); -} -*/ - -/* Too much variance -/// Benchmark multiple > u128::MAX digits -#[divan::bench] -fn factor_multiple_big_uint(bencher: Bencher) { - // max u128 value is 340_282_366_920_938_463_463_374_607_431_768_211_455 - bencher - // this is a range of 3 different BigUints. The range is small due to - // some BigUints being unable to be factorized into prime numbers properly - .with_inputs(|| (768_211_459_u64, 768_211_461_u64)) - .bench_values(|(start_big_uint, end_big_uint)| { - for digit in start_big_uint..=end_big_uint { - let big_uint_str = format!("340282366920938463463374607431768211456{digit}"); - black_box(run_util_function(uumain, &[&big_uint_str])); - } - }); -} -*/ - -#[divan::bench()] -fn factor_table(bencher: Bencher) { - #[cfg(target_os = "linux")] - check_personality(); - - const INPUT_SIZE: usize = 128; - - let inputs = { - // Deterministic RNG; use an explicitly-named RNG to guarantee stability - use rand::{RngCore, SeedableRng}; - const SEED: u64 = 0xdead_bebe_ea75_cafe; // spell-checker:disable-line - let mut rng = rand::rngs::StdRng::seed_from_u64(SEED); - - std::iter::repeat_with(move || { - let mut array = [0u64; INPUT_SIZE]; - for item in &mut array { - *item = rng.next_u64(); - } - array - }) - .take(10) - .collect::>() - }; - - bencher.bench(|| { - for a in &inputs { - for n in a { - divan::black_box(num_prime::nt_funcs::factors(*n, None)); - } - } - }); -} - -#[cfg(target_os = "linux")] -fn check_personality() { - use std::fs; - const ADDR_NO_RANDOMIZE: u64 = 0x0040000; - const PERSONALITY_PATH: &str = "/proc/self/personality"; - - let p_string = fs::read_to_string(PERSONALITY_PATH) - .unwrap_or_else(|_| panic!("Couldn't read '{PERSONALITY_PATH}'")) - .strip_suffix('\n') - .unwrap() - .to_owned(); - - let personality = u64::from_str_radix(&p_string, 16) - .unwrap_or_else(|_| panic!("Expected a hex value for personality, got '{p_string:?}'")); - if personality & ADDR_NO_RANDOMIZE == 0 { - eprintln!( - "WARNING: Benchmarking with ASLR enabled (personality is {personality:x}), results might not be reproducible." - ); - } -} - fn main() { divan::main(); } From 747874911ac601b665436fdbb112f167a6159f42 Mon Sep 17 00:00:00 2001 From: Vesal Joolanejad <85633035+Vesal-J@users.noreply.github.com> Date: Mon, 17 Nov 2025 12:44:22 +0330 Subject: [PATCH 063/182] Enhance mode parsing to support comma-separated mode strings in install command (#9298) * Enhance mode parsing to support comma-separated mode strings in `parse` function. Add tests for comma-separated mode handling in file and directory creation. * Add comprehensive tests for mode parsing in `parse` function, covering numeric, symbolic, and mixed modes, as well as handling of invalid inputs and umask considerations. --------- Co-authored-by: Sylvestre Ledru --- src/uu/install/src/mode.rs | 146 +++++++++++++++++++++++++++++++++- tests/by-util/test_install.rs | 46 +++++++++++ 2 files changed, 188 insertions(+), 4 deletions(-) diff --git a/src/uu/install/src/mode.rs b/src/uu/install/src/mode.rs index 9a2fda317..5c29aaf77 100644 --- a/src/uu/install/src/mode.rs +++ b/src/uu/install/src/mode.rs @@ -9,12 +9,25 @@ use uucore::mode; use uucore::translate; /// Takes a user-supplied string and tries to parse to u16 mode bitmask. +/// Supports comma-separated mode strings like "ug+rwX,o+rX" (same as chmod). pub fn parse(mode_string: &str, considering_dir: bool, umask: u32) -> Result { - if mode_string.chars().any(|c| c.is_ascii_digit()) { - mode::parse_numeric(0, mode_string, considering_dir) - } else { - mode::parse_symbolic(0, mode_string, umask, considering_dir) + // Split by commas and process each mode part sequentially + let mut current_mode: u32 = 0; + + for mode_part in mode_string.split(',') { + let mode_part = mode_part.trim(); + if mode_part.is_empty() { + continue; + } + + current_mode = if mode_part.chars().any(|c| c.is_ascii_digit()) { + mode::parse_numeric(current_mode, mode_part, considering_dir)? + } else { + mode::parse_symbolic(current_mode, mode_part, umask, considering_dir)? + }; } + + Ok(current_mode) } /// chmod a file or directory on UNIX. @@ -42,3 +55,128 @@ pub fn chmod(path: &Path, mode: u32) -> Result<(), ()> { // chmod on Windows only sets the readonly flag, which isn't even honored on directories Ok(()) } + +#[cfg(test)] +#[cfg(not(windows))] +mod tests { + use super::parse; + + #[test] + fn test_parse_numeric_mode() { + // Simple numeric mode + assert_eq!(parse("644", false, 0).unwrap(), 0o644); + assert_eq!(parse("755", false, 0).unwrap(), 0o755); + assert_eq!(parse("777", false, 0).unwrap(), 0o777); + assert_eq!(parse("600", false, 0).unwrap(), 0o600); + } + + #[test] + fn test_parse_numeric_mode_with_operator() { + // Numeric mode with + operator + assert_eq!(parse("+100", false, 0).unwrap(), 0o100); + assert_eq!(parse("+644", false, 0).unwrap(), 0o644); + + // Numeric mode with - operator (starting from 0, so nothing to remove) + assert_eq!(parse("-4", false, 0).unwrap(), 0); + // But if we first set a mode, then remove bits + assert_eq!(parse("644,-4", false, 0).unwrap(), 0o640); + } + + #[test] + fn test_parse_symbolic_mode() { + // Simple symbolic modes + assert_eq!(parse("u+x", false, 0).unwrap(), 0o100); + assert_eq!(parse("g+w", false, 0).unwrap(), 0o020); + assert_eq!(parse("o+r", false, 0).unwrap(), 0o004); + assert_eq!(parse("a+x", false, 0).unwrap(), 0o111); + } + + #[test] + fn test_parse_symbolic_mode_multiple_permissions() { + // Multiple permissions in one mode + assert_eq!(parse("u+rw", false, 0).unwrap(), 0o600); + assert_eq!(parse("ug+rwx", false, 0).unwrap(), 0o770); + assert_eq!(parse("a+rwx", false, 0).unwrap(), 0o777); + } + + #[test] + fn test_parse_comma_separated_modes() { + // Comma-separated mode strings (as mentioned in the doc comment) + assert_eq!(parse("ug+rwX,o+rX", false, 0).unwrap(), 0o664); + assert_eq!(parse("u+rwx,g+rx,o+r", false, 0).unwrap(), 0o754); + assert_eq!(parse("u+w,g+w,o+w", false, 0).unwrap(), 0o222); + } + + #[test] + fn test_parse_comma_separated_with_spaces() { + // Comma-separated with spaces (should be trimmed) + assert_eq!(parse("u+rw, g+rw, o+r", false, 0).unwrap(), 0o664); + assert_eq!(parse(" u+x , g+x ", false, 0).unwrap(), 0o110); + } + + #[test] + fn test_parse_mixed_numeric_and_symbolic() { + // Mix of numeric and symbolic modes + assert_eq!(parse("644,u+x", false, 0).unwrap(), 0o744); + assert_eq!(parse("u+rw,755", false, 0).unwrap(), 0o755); + } + + #[test] + fn test_parse_empty_string() { + // Empty string should return 0 + assert_eq!(parse("", false, 0).unwrap(), 0); + assert_eq!(parse(" ", false, 0).unwrap(), 0); + assert_eq!(parse(",,", false, 0).unwrap(), 0); + } + + #[test] + fn test_parse_with_umask() { + // Test with umask (affects symbolic modes when no level is specified) + let umask = 0o022; + assert_eq!(parse("+w", false, umask).unwrap(), 0o200); + // The umask should be respected for symbolic modes without explicit level + } + + #[test] + fn test_parse_considering_dir() { + // Test directory vs file mode differences + // For directories, X (capital X) should add execute permission + assert_eq!(parse("a+X", true, 0).unwrap(), 0o111); + // For files without execute, X should not add execute + assert_eq!(parse("a+X", false, 0).unwrap(), 0o000); + + // Numeric modes for directories preserve setuid/setgid bits + assert_eq!(parse("755", true, 0).unwrap(), 0o755); + } + + #[test] + fn test_parse_invalid_modes() { + // Invalid numeric mode (too large) + assert!(parse("10000", false, 0).is_err()); + + // Invalid operator + assert!(parse("u*rw", false, 0).is_err()); + + // Invalid symbolic mode + assert!(parse("invalid", false, 0).is_err()); + } + + #[test] + fn test_parse_complex_combinations() { + // Complex real-world examples + assert_eq!(parse("u=rwx,g=rx,o=r", false, 0).unwrap(), 0o754); + // To test removal, we need to first set permissions, then remove them + assert_eq!(parse("644,a-w", false, 0).unwrap(), 0o444); + assert_eq!(parse("644,g-r", false, 0).unwrap(), 0o604); + } + + #[test] + fn test_parse_sequential_application() { + // Test that comma-separated modes are applied sequentially + // First set to 644, then add execute for user + assert_eq!(parse("644,u+x", false, 0).unwrap(), 0o744); + + // First add user write, then set to 755 (should override) + assert_eq!(parse("u+w,755", false, 0).unwrap(), 0o755); + } +} diff --git a/tests/by-util/test_install.rs b/tests/by-util/test_install.rs index 1e78b28da..2a2e7d670 100644 --- a/tests/by-util/test_install.rs +++ b/tests/by-util/test_install.rs @@ -243,6 +243,52 @@ fn test_install_mode_symbolic() { assert_eq!(0o100_003_u32, PermissionsExt::mode(&permissions)); } +#[test] +fn test_install_mode_comma_separated() { + let (at, mut ucmd) = at_and_ucmd!(); + let dir = "target_dir"; + let file = "source_file"; + // Test comma-separated mode like chmod: ug+rwX,o+rX + let mode_arg = "--mode=ug+rwX,o+rX"; + + at.touch(file); + at.mkdir(dir); + ucmd.arg(file).arg(dir).arg(mode_arg).succeeds().no_stderr(); + + let dest_file = &format!("{dir}/{file}"); + assert!(at.file_exists(file)); + assert!(at.file_exists(dest_file)); + let permissions = at.metadata(dest_file).permissions(); + // ug+rwX: For files, X only adds execute if file already has execute (it doesn't here, starting at 0) + // So this adds rw to user and group = 0o660 + // o+rX: For files, X doesn't add execute, so this adds r to others = 0o004 + // Total: 0o664 for file (0o100_664) + assert_eq!(0o100_664_u32, PermissionsExt::mode(&permissions)); +} + +#[test] +fn test_install_mode_comma_separated_directory() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + let dir = "test_dir"; + // Test comma-separated mode for directory creation: ug+rwX,o+rX + let mode_arg = "--mode=ug+rwX,o+rX"; + + scene + .ucmd() + .arg("-d") + .arg(dir) + .arg(mode_arg) + .succeeds() + .no_stderr(); + + assert!(at.dir_exists(dir)); + let permissions = at.metadata(dir).permissions(); + // ug+rwX sets user and group to rwx (0o770), o+rX sets others to r-x (0o005) + // Total: 0o775 for directory (0o040_775) + assert_eq!(0o040_775_u32, PermissionsExt::mode(&permissions)); +} + #[test] fn test_install_mode_symbolic_ignore_umask() { let (at, mut ucmd) = at_and_ucmd!(); From 28abc77aa6ed4e1f505aed5cd091310f1a6b6299 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 17 Nov 2025 18:42:47 +0000 Subject: [PATCH 064/182] chore(deps): update rust crate clap to v4.5.52 --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 199569ec9..6bd339cdd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -345,18 +345,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.51" +version = "4.5.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" +checksum = "aa8120877db0e5c011242f96806ce3c94e0737ab8108532a76a3300a01db2ab8" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.51" +version = "4.5.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" +checksum = "02576b399397b659c26064fbc92a75fede9d18ffd5f80ca1cd74ddab167016e1" dependencies = [ "anstream", "anstyle", From 72c83280343fe019fe7c58f5701363eac6176e30 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Tue, 18 Nov 2025 07:37:40 +0100 Subject: [PATCH 065/182] build-gnu.sh: adjust the PATH for each run Interesting with several uutils/coreutils clones --- util/build-gnu.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/util/build-gnu.sh b/util/build-gnu.sh index f3596c411..f7a6a983c 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -130,6 +130,10 @@ for binary in $(./build-aux/gen-lists-of-programs.sh --list-progs); do } done +# Always update the PATH to test the uutils coreutils instead of the GNU coreutils +# This ensures the correct path is used even if the repository was moved or rebuilt in a different location +sed -i "s/^[[:blank:]]*PATH=.*/ PATH='${UU_BUILD_DIR//\//\\/}\$(PATH_SEPARATOR)'\"\$\$PATH\" \\\/" tests/local.mk + if test -f gnu-built; then echo "GNU build already found. Skip" echo "'rm -f $(pwd)/gnu-built' to force the build" @@ -137,8 +141,6 @@ if test -f gnu-built; then else # Disable useless checks sed -i 's|check-texinfo: $(syntax_checks)|check-texinfo:|' doc/local.mk - # Change the PATH to test the uutils coreutils instead of the GNU coreutils - sed -i "s/^[[:blank:]]*PATH=.*/ PATH='${UU_BUILD_DIR//\//\\/}\$(PATH_SEPARATOR)'\"\$\$PATH\" \\\/" tests/local.mk ./bootstrap --skip-po ./configure --quiet --disable-gcc-warnings --disable-nls --disable-dependency-tracking --disable-bold-man-page-references \ "$([ "${SELINUX_ENABLED}" = 1 ] && echo --with-selinux || echo --without-selinux)" From 055b1e2e7661a181abad5cd74347b1cbe0c020ae Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Tue, 18 Nov 2025 08:42:02 +0100 Subject: [PATCH 066/182] shuf: add benchmarks To test: https://github.com/uutils/coreutils/pull/7585 --- .github/workflows/benchmarks.yml | 1 + Cargo.lock | 2 ++ src/uu/shuf/Cargo.toml | 9 ++++++ src/uu/shuf/benches/shuf_bench.rs | 53 +++++++++++++++++++++++++++++++ 4 files changed, 65 insertions(+) create mode 100644 src/uu/shuf/benches/shuf_bench.rs diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index e1c042e23..37387a211 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -37,6 +37,7 @@ jobs: - { package: uu_numfmt } - { package: uu_rm } - { package: uu_seq } + - { package: uu_shuf } - { package: uu_sort } - { package: uu_split } - { package: uu_tsort } diff --git a/Cargo.lock b/Cargo.lock index 6bd339cdd..22d050ded 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3812,9 +3812,11 @@ name = "uu_shuf" version = "0.4.0" dependencies = [ "clap", + "codspeed-divan-compat", "fluent", "rand 0.9.2", "rand_core 0.9.3", + "tempfile", "uucore", ] diff --git a/src/uu/shuf/Cargo.toml b/src/uu/shuf/Cargo.toml index eea09469b..b67b1d808 100644 --- a/src/uu/shuf/Cargo.toml +++ b/src/uu/shuf/Cargo.toml @@ -27,3 +27,12 @@ fluent = { workspace = true } [[bin]] name = "shuf" path = "src/main.rs" + +[[bench]] +name = "shuf_bench" +harness = false + +[dev-dependencies] +divan = { workspace = true } +tempfile = { workspace = true } +uucore = { workspace = true, features = ["benchmark"] } diff --git a/src/uu/shuf/benches/shuf_bench.rs b/src/uu/shuf/benches/shuf_bench.rs new file mode 100644 index 000000000..62c3be0ba --- /dev/null +++ b/src/uu/shuf/benches/shuf_bench.rs @@ -0,0 +1,53 @@ +// 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 uu_shuf::uumain; +use uucore::benchmark::{run_util_function, setup_test_file, text_data}; + +/// Benchmark shuffling lines from a file +/// Tests the default mode with a large number of lines +#[divan::bench(args = [100_000])] +fn shuf_lines(bencher: Bencher, num_lines: usize) { + let data = text_data::generate_by_lines(num_lines, 80); + let file_path = setup_test_file(&data); + let file_path_str = file_path.to_str().unwrap(); + + bencher.bench(|| { + black_box(run_util_function(uumain, &[file_path_str])); + }); +} + +/// Benchmark shuffling a numeric range with -i +/// Tests the input-range mode which uses a different algorithm +#[divan::bench(args = [1_000_000])] +fn shuf_input_range(bencher: Bencher, range_size: usize) { + let range_arg = format!("1-{range_size}"); + + bencher.bench(|| { + black_box(run_util_function(uumain, &["-i", &range_arg])); + }); +} + +/// Benchmark shuffling with repeat (sampling with replacement) +/// Tests the -r flag combined with -n to output a specific count +#[divan::bench(args = [50_000])] +fn shuf_repeat_sampling(bencher: Bencher, num_lines: usize) { + let data = text_data::generate_by_lines(10_000, 80); + let file_path = setup_test_file(&data); + let file_path_str = file_path.to_str().unwrap(); + let count = format!("{num_lines}"); + + bencher.bench(|| { + black_box(run_util_function( + uumain, + &["-r", "-n", &count, file_path_str], + )); + }); +} + +fn main() { + divan::main(); +} From ea9feef0f127a587c73e5581118d98cd7987b2b1 Mon Sep 17 00:00:00 2001 From: oech3 <> Date: Tue, 18 Nov 2025 16:49:41 +0900 Subject: [PATCH 067/182] build-gnu.sh: Use any profile & cleanup arg --- .github/workflows/GnuTests.yml | 4 ++-- DEVELOPMENT.md | 4 +++- util/build-gnu.sh | 31 ++++++++++--------------------- 3 files changed, 15 insertions(+), 24 deletions(-) diff --git a/.github/workflows/GnuTests.yml b/.github/workflows/GnuTests.yml index a089b6b78..19d5e26ba 100644 --- a/.github/workflows/GnuTests.yml +++ b/.github/workflows/GnuTests.yml @@ -109,7 +109,7 @@ jobs: run: | ## Build binaries cd 'uutils' - bash util/build-gnu.sh --release-build + env PROFILE=release-small bash util/build-gnu.sh ### Run tests as user - name: Run GNU tests @@ -244,7 +244,7 @@ jobs: ### Build - name: Build binaries run: | - lima bash -c "cd ~/work/uutils/ && SELINUX_ENABLED=1 bash util/build-gnu.sh --release-build" + lima bash -c "cd ~/work/uutils/ && SELINUX_ENABLED=1 PROFILE=release-small bash util/build-gnu.sh" ### Run tests as user - name: Generate SELinux tests list diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 912af1ca9..2fcd1a7e7 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -230,7 +230,9 @@ To run uutils against the GNU test suite locally, run the following commands: ```shell bash util/build-gnu.sh # Build uutils with release optimizations -bash util/build-gnu.sh --release-build +env PROFILE=release bash util/build-gnu.sh +# Build uutils with SELinux +env SELINUX_ENABLED=1 bash util/build-gnu.sh bash util/run-gnu-test.sh # To run a single test: bash util/run-gnu-test.sh tests/touch/not-owner.sh # for example diff --git a/util/build-gnu.sh b/util/build-gnu.sh index f3596c411..c70636b8a 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -14,24 +14,18 @@ NPROC=$(command -v gnproc||command -v nproc) READLINK=$(command -v greadlink||command -v readlink) SED=$(command -v gsed||command -v sed) +SYSTEM_TIMEOUT=$(command -v timeout) +SYSTEM_YES=$(command -v yes) + ME="${0}" ME_dir="$(dirname -- "$("${READLINK}" -fm -- "${ME}")")" REPO_main_dir="$(dirname -- "${ME_dir}")" -# Default profile is 'debug' -UU_MAKE_PROFILE='debug' + +: ${PROFILE:=debug} # default profile +export PROFILE CARGO_FEATURE_FLAGS="" -for arg in "$@" -do - if [ "$arg" == "--release-build" ]; then - UU_MAKE_PROFILE='release' - break - fi -done - -echo "UU_MAKE_PROFILE='${UU_MAKE_PROFILE}'" - ### * config (from environment with fallback defaults); note: GNU is expected to be a sibling repo directory path_UUTILS=${path_UUTILS:-${REPO_main_dir}} @@ -39,11 +33,6 @@ path_GNU="$("${READLINK}" -fm -- "${path_GNU:-${path_UUTILS}/../gnu}")" ### -SYSTEM_TIMEOUT=$(command -v timeout) -SYSTEM_YES=$(command -v yes) - -### - release_tag_GNU="v9.9" # check if the GNU coreutils has been cloned, if not print instructions @@ -71,9 +60,9 @@ echo "path_GNU='${path_GNU}'" ### if [[ ! -z "$CARGO_TARGET_DIR" ]]; then -UU_BUILD_DIR="${CARGO_TARGET_DIR}/${UU_MAKE_PROFILE}" +UU_BUILD_DIR="${CARGO_TARGET_DIR}/${PROFILE}" else -UU_BUILD_DIR="${path_UUTILS}/target/${UU_MAKE_PROFILE}" +UU_BUILD_DIR="${path_UUTILS}/target/${PROFILE}" fi echo "UU_BUILD_DIR='${UU_BUILD_DIR}'" @@ -105,9 +94,9 @@ fi cd - # Pass the feature flags to make, which will pass them to cargo -"${MAKE}" PROFILE="${UU_MAKE_PROFILE}" CARGOFLAGS="${CARGO_FEATURE_FLAGS}" +"${MAKE}" PROFILE="${PROFILE}" CARGOFLAGS="${CARGO_FEATURE_FLAGS}" # min test for SELinux -[ "${SELINUX_ENABLED}" = 1 ] && touch g && "${UU_MAKE_PROFILE}"/stat -c%C g && rm g +[ "${SELINUX_ENABLED}" = 1 ] && touch g && "${PROFILE}"/stat -c%C g && rm g cp "${UU_BUILD_DIR}/install" "${UU_BUILD_DIR}/ginstall" # The GNU tests rename this script before running, to avoid confusion with the make target # Create *sum binaries From 07098903284c3bdc216283f141cabc18d957cbb2 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Tue, 18 Nov 2025 09:37:38 +0100 Subject: [PATCH 068/182] replace number_prefix by unit-prefix See https://rustsec.org/advisories/RUSTSEC-2025-0119 --- Cargo.lock | 12 +++--------- Cargo.toml | 2 +- fuzz/Cargo.lock | 14 +++++++------- src/uucore/Cargo.toml | 2 +- src/uucore/src/lib/features/format/human.rs | 2 +- 5 files changed, 13 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 22d050ded..3c8aa5805 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1950,12 +1950,6 @@ dependencies = [ "libc", ] -[[package]] -name = "number_prefix" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" - [[package]] name = "once_cell" version = "1.21.3" @@ -2958,9 +2952,9 @@ checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" [[package]] name = "unit-prefix" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "323402cff2dd658f39ca17c789b502021b3f18707c91cdf22e3838e1b4023817" +checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" [[package]] name = "unty" @@ -4208,7 +4202,6 @@ dependencies = [ "memchr", "nix", "num-traits", - "number_prefix", "os_display", "phf", "procfs", @@ -4221,6 +4214,7 @@ dependencies = [ "thiserror 2.0.17", "time", "unic-langid", + "unit-prefix", "utmp-classic", "uucore_procs", "walkdir", diff --git a/Cargo.toml b/Cargo.toml index 3d58e3f02..79bff3955 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -349,7 +349,6 @@ notify = { version = "=8.2.0", features = ["macos_kqueue"] } num-bigint = "0.4.4" num-prime = "0.4.4" num-traits = "0.2.19" -number_prefix = "0.4" onig = { version = "~6.5.1", default-features = false } parse_datetime = "0.13.0" phf = "0.13.1" @@ -373,6 +372,7 @@ textwrap = { version = "0.16.1", features = ["terminal_size"] } thiserror = "2.0.3" time = { version = "0.3.36" } unicode-width = "0.2.0" +unit-prefix = "0.5" utmp-classic = "0.1.6" uutils_term_grid = "0.7" walkdir = "2.5" diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index 63c3c1209..58ee595df 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -1017,12 +1017,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "number_prefix" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" - [[package]] name = "objc2" version = "0.6.3" @@ -1578,6 +1572,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +[[package]] +name = "unit-prefix" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" + [[package]] name = "utf16_iter" version = "1.0.5" @@ -1784,7 +1784,6 @@ dependencies = [ "memchr", "nix", "num-traits", - "number_prefix", "os_display", "phf", "procfs", @@ -1794,6 +1793,7 @@ dependencies = [ "sm3", "thiserror", "unic-langid", + "unit-prefix", "uucore_procs", "wild", "winapi-util", diff --git a/src/uucore/Cargo.toml b/src/uucore/Cargo.toml index e9afb5542..242f25903 100644 --- a/src/uucore/Cargo.toml +++ b/src/uucore/Cargo.toml @@ -26,7 +26,7 @@ bstr = { workspace = true } chrono = { workspace = true, optional = true } clap = { workspace = true } uucore_procs = { workspace = true } -number_prefix = { workspace = true } +unit-prefix = { workspace = true } phf = { workspace = true } dns-lookup = { workspace = true, optional = true } dunce = { version = "1.0.4", optional = true } diff --git a/src/uucore/src/lib/features/format/human.rs b/src/uucore/src/lib/features/format/human.rs index 3c80e0b19..7777103b9 100644 --- a/src/uucore/src/lib/features/format/human.rs +++ b/src/uucore/src/lib/features/format/human.rs @@ -9,7 +9,7 @@ //! //! Format sizes like gnulibs human_readable() would -use number_prefix::NumberPrefix; +use unit_prefix::NumberPrefix; #[derive(Copy, Clone, PartialEq)] pub enum SizeFormat { From 8c9bb168573ee163b3b906362c656f603de43102 Mon Sep 17 00:00:00 2001 From: Dorian Peron Date: Thu, 30 Oct 2025 18:23:35 +0100 Subject: [PATCH 069/182] test(cksum): Add tests for BLAKE2b --length sanitization --- tests/by-util/test_cksum.rs | 33 ++++++++++++++----- .../cksum/length_larger_than_512.expected | 2 -- 2 files changed, 25 insertions(+), 10 deletions(-) delete mode 100644 tests/fixtures/cksum/length_larger_than_512.expected diff --git a/tests/by-util/test_cksum.rs b/tests/by-util/test_cksum.rs index d966e4b1f..4b9459ef5 100644 --- a/tests/by-util/test_cksum.rs +++ b/tests/by-util/test_cksum.rs @@ -774,14 +774,31 @@ fn test_blake2b_length() { #[test] fn test_blake2b_length_greater_than_512() { - new_ucmd!() - .arg("--length=1024") - .arg("--algorithm=blake2b") - .arg("lorem_ipsum.txt") - .arg("alice_in_wonderland.txt") - .fails_with_code(1) - .no_stdout() - .stderr_is_fixture("length_larger_than_512.expected"); + for l in ["513", "1024", "73786976294838206464"] { + new_ucmd!() + .arg("--algorithm=blake2b") + .arg("--length") + .arg(l) + .arg("lorem_ipsum.txt") + .fails_with_code(1) + .no_stdout() + .stderr_contains(format!("invalid length: '{l}'")) + .stderr_contains("maximum digest length for 'BLAKE2b' is 512 bits"); + } +} + +#[test] +fn test_blake2b_length_nan() { + for l in ["foo", "512x", "x512", "0xff"] { + new_ucmd!() + .arg("--algorithm=blake2b") + .arg("--length") + .arg(l) + .arg("lorem_ipsum.txt") + .fails_with_code(1) + .no_stdout() + .stderr_contains(format!("invalid length: '{l}'")); + } } #[test] diff --git a/tests/fixtures/cksum/length_larger_than_512.expected b/tests/fixtures/cksum/length_larger_than_512.expected deleted file mode 100644 index 8b5d3d4c2..000000000 --- a/tests/fixtures/cksum/length_larger_than_512.expected +++ /dev/null @@ -1,2 +0,0 @@ -cksum: invalid length: '1024' -cksum: maximum digest length for 'BLAKE2b' is 512 bits From 885082ce140235ca93795896cdded8297dd5bfcd Mon Sep 17 00:00:00 2001 From: Dorian Peron Date: Thu, 30 Oct 2025 18:26:05 +0100 Subject: [PATCH 070/182] util(cksum): Fix BLAKE2b --length sanitization --- src/uucore/src/lib/features/checksum.rs | 49 ++++++++++++++----------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/src/uucore/src/lib/features/checksum.rs b/src/uucore/src/lib/features/checksum.rs index 324dba7b3..6c67b68ac 100644 --- a/src/uucore/src/lib/features/checksum.rs +++ b/src/uucore/src/lib/features/checksum.rs @@ -219,16 +219,23 @@ pub enum ChecksumError { StrictNotCheck, #[error("the --quiet option is meaningful only when verifying checksums")] QuietNotCheck, + + // --length sanitization errors #[error("--length required for {}", .0.quote())] LengthRequired(String), #[error("invalid length: {}", .0.quote())] InvalidLength(String), + #[error("maximum digest length for {} is 512 bits", .0.quote())] + LengthTooBigForBlake(String), + #[error("length is not a multiple of 8")] + LengthNotMultipleOf8, #[error("digest length for {} must be 224, 256, 384, or 512", .0.quote())] InvalidLengthForSha(String), #[error("--algorithm={0} requires specifying --length 224, 256, 384, or 512")] LengthRequiredForSha(String), #[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")] @@ -1243,34 +1250,32 @@ pub fn calculate_blake2b_length(length: usize) -> UResult> { /// Calculates the length of the digest. pub fn calculate_blake2b_length_str(length: &str) -> UResult> { - match length.parse() { + // Blake2b's length is parsed in an u64. + match length.parse::() { Ok(0) => Ok(None), - Ok(n) if n % 8 != 0 => { - show_error!("{}", ChecksumError::InvalidLength(length.into())); - Err(io::Error::new(io::ErrorKind::InvalidInput, "length is not a multiple of 8").into()) - } + + // Error cases Ok(n) if n > 512 => { show_error!("{}", ChecksumError::InvalidLength(length.into())); - Err(io::Error::new( - io::ErrorKind::InvalidInput, - format!( - "maximum digest length for {} is 512 bits", - "BLAKE2b".quote() - ), - ) - .into()) + Err(ChecksumError::LengthTooBigForBlake("BLAKE2b".into()).into()) } - Ok(n) => { - // Divide by 8, as our blake2b implementation expects bytes instead of bits. - if n == 512 { - // When length is 512, it is blake2b's default. - // So, don't show it - Ok(None) - } else { - Ok(Some(n / 8)) - } + Err(e) if *e.kind() == IntErrorKind::PosOverflow => { + show_error!("{}", ChecksumError::InvalidLength(length.into())); + Err(ChecksumError::LengthTooBigForBlake("BLAKE2b".into()).into()) } Err(_) => Err(ChecksumError::InvalidLength(length.into()).into()), + + Ok(n) if n % 8 != 0 => { + show_error!("{}", ChecksumError::InvalidLength(length.into())); + Err(ChecksumError::LengthNotMultipleOf8.into()) + } + + // Valid cases + + // When length is 512, it is blake2b's default. So, don't show it + Ok(512) => Ok(None), + // Divide by 8, as our blake2b implementation expects bytes instead of bits. + Ok(n) => Ok(Some(n / 8)), } } From 9b6dc9675c82e78761020e8bd984e83fb1bb1855 Mon Sep 17 00:00:00 2001 From: Dorian Peron Date: Fri, 31 Oct 2025 03:10:10 +0100 Subject: [PATCH 071/182] checksum: Introduce `AlgoKind` enum to rely less on string comparison --- src/uu/cksum/src/cksum.rs | 23 +- src/uu/hashsum/src/hashsum.rs | 80 ++-- src/uucore/src/lib/features/checksum.rs | 472 +++++++++++++++--------- 3 files changed, 337 insertions(+), 238 deletions(-) diff --git a/src/uu/cksum/src/cksum.rs b/src/uu/cksum/src/cksum.rs index c7a3e969b..cdd9de33f 100644 --- a/src/uu/cksum/src/cksum.rs +++ b/src/uu/cksum/src/cksum.rs @@ -14,10 +14,10 @@ use std::iter; use std::path::Path; use uucore::checksum::{ ALGORITHM_OPTIONS_BLAKE2B, ALGORITHM_OPTIONS_BSD, ALGORITHM_OPTIONS_CRC, - ALGORITHM_OPTIONS_CRC32B, ALGORITHM_OPTIONS_SHA2, ALGORITHM_OPTIONS_SHA3, - ALGORITHM_OPTIONS_SYSV, ChecksumError, ChecksumOptions, ChecksumVerbose, HashAlgorithm, - LEGACY_ALGORITHMS, SUPPORTED_ALGORITHMS, calculate_blake2b_length_str, detect_algo, - digest_reader, perform_checksum_validation, sanitize_sha2_sha3_length_str, + ALGORITHM_OPTIONS_CRC32B, ALGORITHM_OPTIONS_SYSV, AlgoKind, ChecksumError, ChecksumOptions, + ChecksumVerbose, HashAlgorithm, LEGACY_ALGORITHMS, SUPPORTED_ALGORITHMS, + calculate_blake2b_length_str, detect_algo, digest_reader, perform_checksum_validation, + sanitize_sha2_sha3_length_str, }; use uucore::translate; @@ -368,7 +368,7 @@ fn figure_out_output_format( /// Sanitize the `--length` argument depending on `--algorithm` and `--length`. fn maybe_sanitize_length( - algo_cli: Option<&str>, + algo_cli: Option, input_length: Option<&str>, ) -> UResult> { match (algo_cli, input_length) { @@ -376,12 +376,12 @@ fn maybe_sanitize_length( (_, None) => Ok(None), // For SHA2 and SHA3, if a length is provided, ensure it is correct. - (Some(algo @ (ALGORITHM_OPTIONS_SHA2 | ALGORITHM_OPTIONS_SHA3)), Some(s_len)) => { + (Some(algo @ (AlgoKind::Sha2 | AlgoKind::Sha3)), Some(s_len)) => { sanitize_sha2_sha3_length_str(algo, s_len).map(Some) } // For BLAKE2b, if a length is provided, validate it. - (Some(ALGORITHM_OPTIONS_BLAKE2B), Some(len)) => calculate_blake2b_length_str(len), + (Some(AlgoKind::Blake2b), Some(len)) => calculate_blake2b_length_str(len), // For any other provided algorithm, check if length is 0. // Otherwise, this is an error. @@ -398,7 +398,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let algo_cli = matches .get_one::(options::ALGORITHM) - .map(String::as_str); + .map(AlgoKind::from_cksum) + .transpose()?; let input_length = matches .get_one::(options::LENGTH) @@ -415,7 +416,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { if check { // cksum does not support '--check'ing legacy algorithms - if algo_cli.is_some_and(|algo_name| LEGACY_ALGORITHMS.contains(&algo_name)) { + if algo_cli.is_some_and(AlgoKind::is_legacy) { return Err(ChecksumError::AlgorithmNotSupportedWithCheck.into()); } @@ -448,11 +449,11 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // Not --check // Set the default algorithm to CRC when not '--check'ing. - let algo_name = algo_cli.unwrap_or(ALGORITHM_OPTIONS_CRC); + let algo_kind = algo_cli.unwrap_or(AlgoKind::Crc); let (tag, binary) = handle_tag_text_binary_flags(std::env::args_os())?; - let algo = detect_algo(algo_name, length)?; + let algo = detect_algo(algo_kind, length)?; let line_ending = LineEnding::from_zero_flag(matches.get_flag(options::ZERO)); let output_format = figure_out_output_format( diff --git a/src/uu/hashsum/src/hashsum.rs b/src/uu/hashsum/src/hashsum.rs index 7edc916fb..4e0206252 100644 --- a/src/uu/hashsum/src/hashsum.rs +++ b/src/uu/hashsum/src/hashsum.rs @@ -15,19 +15,17 @@ use std::io::{BufReader, Read, stdin}; use std::iter; use std::num::ParseIntError; use std::path::Path; -use uucore::checksum::ChecksumError; use uucore::checksum::ChecksumOptions; use uucore::checksum::ChecksumVerbose; -use uucore::checksum::HashAlgorithm; use uucore::checksum::calculate_blake2b_length; -use uucore::checksum::create_sha3; use uucore::checksum::detect_algo; use uucore::checksum::digest_reader; use uucore::checksum::escape_filename; use uucore::checksum::perform_checksum_validation; +use uucore::checksum::{AlgoKind, ChecksumError}; use uucore::error::{UResult, strip_errno}; use uucore::format_usage; -use uucore::sum::{Digest, Sha3_224, Sha3_256, Sha3_384, Sha3_512, Shake128, Shake256}; +use uucore::sum::Digest; use uucore::translate; const NAME: &str = "hashsum"; @@ -63,10 +61,10 @@ struct Options<'a> { /// the output length in bits or an Err if multiple hash algorithms are specified or if a /// required flag is missing. #[allow(clippy::cognitive_complexity)] -fn create_algorithm_from_flags(matches: &ArgMatches) -> UResult { - let mut alg: Option = None; +fn create_algorithm_from_flags(matches: &ArgMatches) -> UResult<(AlgoKind, Option)> { + let mut alg: Option<(AlgoKind, Option)> = None; - let mut set_or_err = |new_alg: HashAlgorithm| -> UResult<()> { + let mut set_or_err = |new_alg: (AlgoKind, Option)| -> UResult<()> { if alg.is_some() { return Err(ChecksumError::CombineMultipleAlgorithms.into()); } @@ -75,80 +73,57 @@ fn create_algorithm_from_flags(matches: &ArgMatches) -> UResult { }; if matches.get_flag("md5") { - set_or_err(detect_algo("md5sum", None)?)?; + set_or_err((AlgoKind::Md5, None))?; } if matches.get_flag("sha1") { - set_or_err(detect_algo("sha1sum", None)?)?; + set_or_err((AlgoKind::Sha1, None))?; } if matches.get_flag("sha224") { - set_or_err(detect_algo("sha224sum", None)?)?; + set_or_err((AlgoKind::Sha224, None))?; } if matches.get_flag("sha256") { - set_or_err(detect_algo("sha256sum", None)?)?; + set_or_err((AlgoKind::Sha256, None))?; } if matches.get_flag("sha384") { - set_or_err(detect_algo("sha384sum", None)?)?; + set_or_err((AlgoKind::Sha384, None))?; } if matches.get_flag("sha512") { - set_or_err(detect_algo("sha512sum", None)?)?; + set_or_err((AlgoKind::Sha512, None))?; } if matches.get_flag("b2sum") { - set_or_err(detect_algo("b2sum", None)?)?; + set_or_err((AlgoKind::Blake2b, None))?; } if matches.get_flag("b3sum") { - set_or_err(detect_algo("b3sum", None)?)?; + set_or_err((AlgoKind::Blake3, None))?; } if matches.get_flag("sha3") { match matches.get_one::("bits") { - Some(bits) => set_or_err(create_sha3(*bits)?)?, + Some(bits @ (224 | 256 | 384 | 512)) => set_or_err((AlgoKind::Sha3, Some(*bits)))?, + Some(bits) => return Err(ChecksumError::InvalidLengthForSha(bits.to_string()).into()), None => return Err(ChecksumError::LengthRequired("SHA3".into()).into()), } } if matches.get_flag("sha3-224") { - set_or_err(HashAlgorithm { - name: "SHA3-224", - create_fn: Box::new(|| Box::new(Sha3_224::new())), - bits: 224, - })?; + set_or_err((AlgoKind::Sha3, Some(224)))?; } if matches.get_flag("sha3-256") { - set_or_err(HashAlgorithm { - name: "SHA3-256", - create_fn: Box::new(|| Box::new(Sha3_256::new())), - bits: 256, - })?; + set_or_err((AlgoKind::Sha3, Some(256)))?; } if matches.get_flag("sha3-384") { - set_or_err(HashAlgorithm { - name: "SHA3-384", - create_fn: Box::new(|| Box::new(Sha3_384::new())), - bits: 384, - })?; + set_or_err((AlgoKind::Sha3, Some(384)))?; } if matches.get_flag("sha3-512") { - set_or_err(HashAlgorithm { - name: "SHA3-512", - create_fn: Box::new(|| Box::new(Sha3_512::new())), - bits: 512, - })?; + set_or_err((AlgoKind::Sha3, Some(512)))?; } if matches.get_flag("shake128") { match matches.get_one::("bits") { - Some(bits) => set_or_err(HashAlgorithm { - name: "SHAKE128", - create_fn: Box::new(|| Box::new(Shake128::new())), - bits: *bits, - })?, + Some(bits) => set_or_err((AlgoKind::Shake128, Some(*bits)))?, None => return Err(ChecksumError::LengthRequired("SHAKE128".into()).into()), } } if matches.get_flag("shake256") { match matches.get_one::("bits") { - Some(bits) => set_or_err(HashAlgorithm { - name: "SHAKE256", - create_fn: Box::new(|| Box::new(Shake256::new())), - bits: *bits, - })?, + Some(bits) => set_or_err((AlgoKind::Shake256, Some(*bits)))?, None => return Err(ChecksumError::LengthRequired("SHAKE256".into()).into()), } } @@ -198,10 +173,10 @@ pub fn uumain(mut args: impl uucore::Args) -> UResult<()> { None => None, }; - let algo = if is_hashsum_bin { + let (algo_kind, length) = if is_hashsum_bin { create_algorithm_from_flags(&matches)? } else { - detect_algo(&binary_name, length)? + (AlgoKind::from_bin_name(&binary_name)?, length) }; let binary = if matches.get_flag("binary") { @@ -255,12 +230,7 @@ pub fn uumain(mut args: impl uucore::Args) -> UResult<()> { }; // Execute the checksum validation - return perform_checksum_validation( - input.iter().copied(), - Some(algo.name), - Some(algo.bits), - opts, - ); + return perform_checksum_validation(input.iter().copied(), Some(algo_kind), length, opts); } else if quiet { return Err(ChecksumError::QuietNotCheck.into()); } else if strict { @@ -273,6 +243,8 @@ pub fn uumain(mut args: impl uucore::Args) -> UResult<()> { .unwrap_or(&false); let zero = matches.get_flag("zero"); + let algo = detect_algo(algo_kind, length)?; + let opts = Options { algoname: algo.name, digest: (algo.create_fn)(), diff --git a/src/uucore/src/lib/features/checksum.rs b/src/uucore/src/lib/features/checksum.rs index 6c67b68ac..e71d8135c 100644 --- a/src/uucore/src/lib/features/checksum.rs +++ b/src/uucore/src/lib/features/checksum.rs @@ -60,11 +60,13 @@ pub const SUPPORTED_ALGORITHMS: [&str; 17] = [ ALGORITHM_OPTIONS_SHA3, ALGORITHM_OPTIONS_BLAKE2B, ALGORITHM_OPTIONS_SM3, - // Extra algorithms that are not valid `cksum --algorithm` + // Legacy aliases for -a sha2 -l xxx ALGORITHM_OPTIONS_SHA224, ALGORITHM_OPTIONS_SHA256, ALGORITHM_OPTIONS_SHA384, ALGORITHM_OPTIONS_SHA512, + // Extra algorithms that are not valid `cksum --algorithm` as per GNU. + // TODO: Should we keep them or drop them to align our support with GNU ? ALGORITHM_OPTIONS_BLAKE3, ALGORITHM_OPTIONS_SHAKE128, ALGORITHM_OPTIONS_SHAKE256, @@ -77,6 +79,139 @@ pub const LEGACY_ALGORITHMS: [&str; 4] = [ ALGORITHM_OPTIONS_CRC32B, ]; +/// Represents an algorithm kind. In some cases, it is not sufficient by itself +/// to know which algorithm to use exactly, because it lacks a digest length, +/// which is why [`SizedAlgoKind`] exists. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AlgoKind { + Sysv, + Bsd, + Crc, + Crc32b, + Md5, + Sm3, + Sha1, + Sha2, + Sha3, + Blake2b, + + // Available in cksum for backward compatibility + Sha224, + Sha256, + Sha384, + Sha512, + + // Not available in cksum + Shake128, + Shake256, + Blake3, +} + +impl AlgoKind { + /// Parses an [`AlgoKind`] from a string, only accepting valid cksum + /// `--algorithm` values. + pub fn from_cksum(algo: impl AsRef) -> UResult { + use AlgoKind::*; + Ok(match algo.as_ref() { + ALGORITHM_OPTIONS_SYSV => Sysv, + ALGORITHM_OPTIONS_BSD => Bsd, + ALGORITHM_OPTIONS_CRC => Crc, + ALGORITHM_OPTIONS_CRC32B => Crc32b, + ALGORITHM_OPTIONS_MD5 => Md5, + ALGORITHM_OPTIONS_SHA1 => Sha1, + ALGORITHM_OPTIONS_SHA2 => Sha2, + ALGORITHM_OPTIONS_SHA3 => Sha3, + ALGORITHM_OPTIONS_BLAKE2B => Blake2b, + ALGORITHM_OPTIONS_SM3 => Sm3, + + // For backward compatibility + ALGORITHM_OPTIONS_SHA224 => Sha224, + ALGORITHM_OPTIONS_SHA256 => Sha256, + ALGORITHM_OPTIONS_SHA384 => Sha384, + ALGORITHM_OPTIONS_SHA512 => Sha512, + _ => return Err(ChecksumError::UnknownAlgorithm(algo.as_ref().to_string()).into()), + }) + } + + /// Parses an algo kind from a string, accepting standalone binary names. + pub fn from_bin_name(algo: impl AsRef) -> UResult { + use AlgoKind::*; + Ok(match algo.as_ref() { + "md5sum" => Md5, + "sha1sum" => Sha1, + "sha224sum" => Sha224, + "sha256sum" => Sha256, + "sha384sum" => Sha384, + "sha512sum" => Sha512, + "sha3sum" => Sha3, + "b2sum" => Blake2b, + + _ => return Err(ChecksumError::UnknownAlgorithm(algo.as_ref().to_string()).into()), + }) + } + + /// Returns a string corresponding to the algorithm kind. + pub fn to_uppercase(self) -> &'static str { + use AlgoKind::*; + match self { + // Legacy algorithms + Sysv => "SYSV", + Bsd => "BSD", + Crc => "CRC", + Crc32b => "CRC32B", + + Md5 => "MD5", + Sm3 => "SM3", + Sha1 => "SHA1", + Sha2 => "SHA2", + Sha3 => "SHA3", + Blake2b => "BLAKE2b", // Note the lowercase b in the end here. + + // For backward compatibility + Sha224 => "SHA224", + Sha256 => "SHA256", + Sha384 => "SHA384", + Sha512 => "SHA512", + + Shake128 => "SHAKE128", + Shake256 => "SHAKE256", + Blake3 => "BLAKE3", + } + } + + /// Returns a string corresponding to the algorithm option in cksum `-a` + pub fn to_lowercase(self) -> &'static str { + use AlgoKind::*; + match self { + Sysv => "sysv", + Bsd => "bsd", + Crc => "crc", + Crc32b => "crc32b", + Md5 => "md5", + Sm3 => "sm3", + Sha1 => "sha1", + Sha2 => "sha2", + Sha3 => "sha3", + Blake2b => "blake2b", + + // For backward compatibility + Sha224 => "sha224", + Sha256 => "sha256", + Sha384 => "sha384", + Sha512 => "sha512", + + Shake128 => "shake128", + Shake256 => "shake256", + Blake3 => "blake3", + } + } + + pub fn is_legacy(self) -> bool { + use AlgoKind::*; + matches!(self, Sysv | Bsd | Crc | Crc32b) + } +} + pub struct HashAlgorithm { pub name: &'static str, pub create_fn: Box Box>, @@ -402,43 +537,43 @@ fn print_file_report( } } -pub fn detect_algo(algo: &str, length: Option) -> UResult { +pub fn detect_algo(algo: AlgoKind, length: Option) -> UResult { match algo { - ALGORITHM_OPTIONS_SYSV => Ok(HashAlgorithm { + AlgoKind::Sysv => Ok(HashAlgorithm { name: ALGORITHM_OPTIONS_SYSV, create_fn: Box::new(|| Box::new(SysV::new())), bits: 512, }), - ALGORITHM_OPTIONS_BSD => Ok(HashAlgorithm { + AlgoKind::Bsd => Ok(HashAlgorithm { name: ALGORITHM_OPTIONS_BSD, create_fn: Box::new(|| Box::new(Bsd::new())), bits: 1024, }), - ALGORITHM_OPTIONS_CRC => Ok(HashAlgorithm { + AlgoKind::Crc => Ok(HashAlgorithm { name: ALGORITHM_OPTIONS_CRC, create_fn: Box::new(|| Box::new(Crc::new())), bits: 256, }), - ALGORITHM_OPTIONS_CRC32B => Ok(HashAlgorithm { + AlgoKind::Crc32b => Ok(HashAlgorithm { name: ALGORITHM_OPTIONS_CRC32B, create_fn: Box::new(|| Box::new(CRC32B::new())), bits: 32, }), - ALGORITHM_OPTIONS_MD5 | "md5sum" => Ok(HashAlgorithm { + AlgoKind::Md5 => Ok(HashAlgorithm { name: ALGORITHM_OPTIONS_MD5, create_fn: Box::new(|| Box::new(Md5::new())), bits: 128, }), - ALGORITHM_OPTIONS_SHA1 | "sha1sum" => Ok(HashAlgorithm { + AlgoKind::Sha1 => Ok(HashAlgorithm { name: ALGORITHM_OPTIONS_SHA1, create_fn: Box::new(|| Box::new(Sha1::new())), bits: 160, }), - ALGORITHM_OPTIONS_SHA224 | "sha224sum" => Ok(create_sha2(224)?), - ALGORITHM_OPTIONS_SHA256 | "sha256sum" => Ok(create_sha2(256)?), - ALGORITHM_OPTIONS_SHA384 | "sha384sum" => Ok(create_sha2(384)?), - ALGORITHM_OPTIONS_SHA512 | "sha512sum" => Ok(create_sha2(512)?), - ALGORITHM_OPTIONS_BLAKE2B | "b2sum" => { + AlgoKind::Sha224 => Ok(create_sha2(224)?), + AlgoKind::Sha256 => Ok(create_sha2(256)?), + AlgoKind::Sha384 => Ok(create_sha2(384)?), + AlgoKind::Sha512 => Ok(create_sha2(512)?), + AlgoKind::Blake2b => { // Set default length to 512 if None let bits = length.unwrap_or(512); if bits == 512 { @@ -455,48 +590,50 @@ pub fn detect_algo(algo: &str, length: Option) -> UResult }) } } - ALGORITHM_OPTIONS_BLAKE3 | "b3sum" => Ok(HashAlgorithm { + AlgoKind::Blake3 => Ok(HashAlgorithm { name: ALGORITHM_OPTIONS_BLAKE3, create_fn: Box::new(|| Box::new(Blake3::new())), bits: 256, }), - ALGORITHM_OPTIONS_SM3 => Ok(HashAlgorithm { + AlgoKind::Sm3 => Ok(HashAlgorithm { name: ALGORITHM_OPTIONS_SM3, create_fn: Box::new(|| Box::new(Sm3::new())), bits: 512, }), - algo @ (ALGORITHM_OPTIONS_SHAKE128 | "shake128sum") => { - let bits = length.ok_or(ChecksumError::LengthRequired(algo.to_ascii_uppercase()))?; + AlgoKind::Shake128 => { + let bits = length.ok_or(ChecksumError::LengthRequired( + algo.to_uppercase().to_string(), + ))?; Ok(HashAlgorithm { name: ALGORITHM_OPTIONS_SHAKE128, create_fn: Box::new(|| Box::new(Shake128::new())), bits, }) } - algo @ (ALGORITHM_OPTIONS_SHAKE256 | "shake256sum") => { - let bits = length.ok_or(ChecksumError::LengthRequired(algo.to_ascii_uppercase()))?; + AlgoKind::Shake256 => { + let bits = length.ok_or(ChecksumError::LengthRequired( + algo.to_uppercase().to_string(), + ))?; Ok(HashAlgorithm { name: ALGORITHM_OPTIONS_SHAKE256, create_fn: Box::new(|| Box::new(Shake256::new())), bits, }) } - algo @ ALGORITHM_OPTIONS_SHA2 => { - let bits = validate_sha2_sha3_length(algo, length)?; - create_sha2(bits) - } - algo @ ALGORITHM_OPTIONS_SHA3 => { - let bits = validate_sha2_sha3_length(algo, length)?; - create_sha3(bits) + AlgoKind::Sha2 => { + let len = validate_sha2_sha3_length(algo, length)?; + create_sha2(len) } + AlgoKind::Sha3 => { + let len = validate_sha2_sha3_length(algo, length)?; + create_sha3(len) + } // TODO: `hashsum` specific, to remove once hashsum is removed. + // algo @ ("sha3-224" | "sha3-256" | "sha3-384" | "sha3-512") => { + // let bits: usize = algo.strip_prefix("sha3-").unwrap().parse().unwrap(); + // create_sha3(bits) + // } - // TODO: `hashsum` specific, to remove once hashsum is removed. - algo @ ("sha3-224" | "sha3-256" | "sha3-384" | "sha3-512") => { - let bits: usize = algo.strip_prefix("sha3-").unwrap().parse().unwrap(); - create_sha3(bits) - } - - algo => Err(ChecksumError::UnknownAlgorithm(algo.into()).into()), + // algo => Err(ChecksumError::UnknownAlgorithm(algo.into()).into()), } } @@ -843,11 +980,14 @@ fn get_input_file(filename: &OsStr) -> UResult> { /// Gets the algorithm name and length from the `LineInfo` if the algo-based format is matched. fn identify_algo_name_and_length( line_info: &LineInfo, - algo_name_input: Option<&str>, + algo_name_input: Option, last_algo: &mut Option, -) -> Result<(String, Option), LineCheckError> { +) -> Result<(AlgoKind, Option), LineCheckError> { let algo_from_line = line_info.algo_name.clone().unwrap_or_default(); - let line_algo = algo_from_line.to_lowercase(); + let Ok(line_algo) = AlgoKind::from_cksum(algo_from_line.to_lowercase()) else { + // Unknown algorithm + return Err(LineCheckError::ImproperlyFormatted); + }; *last_algo = Some(algo_from_line); // check if we are called with XXXsum (example: md5sum) but we detected a @@ -855,31 +995,21 @@ fn identify_algo_name_and_length( // // Also handle the case cksum -s sm3 but the file contains other formats if let Some(algo_name_input) = algo_name_input { - match (algo_name_input, line_algo.as_str()) { + match (algo_name_input, line_algo) { (l, r) if l == r => (), // Edge case for SHA2, which matches SHA(224|256|384|512) ( - ALGORITHM_OPTIONS_SHA2, - ALGORITHM_OPTIONS_SHA224 - | ALGORITHM_OPTIONS_SHA256 - | ALGORITHM_OPTIONS_SHA384 - | ALGORITHM_OPTIONS_SHA512, + AlgoKind::Sha2, + AlgoKind::Sha224 | AlgoKind::Sha256 | AlgoKind::Sha384 | AlgoKind::Sha512, ) => (), _ => return Err(LineCheckError::ImproperlyFormatted), } } - if !SUPPORTED_ALGORITHMS.contains(&line_algo.as_str()) { - // Not supported algo, leave early - return Err(LineCheckError::ImproperlyFormatted); - } - let bytes = if let Some(bitlen) = line_info.algo_bit_len { - match line_algo.as_str() { - ALGORITHM_OPTIONS_BLAKE2B if bitlen % 8 == 0 => Some(bitlen / 8), - ALGORITHM_OPTIONS_SHA2 | ALGORITHM_OPTIONS_SHA3 - if [224, 256, 384, 512].contains(&bitlen) => - { + match line_algo { + AlgoKind::Blake2b if bitlen % 8 == 0 => Some(bitlen / 8), + AlgoKind::Sha2 | AlgoKind::Sha3 if [224, 256, 384, 512].contains(&bitlen) => { Some(bitlen) } // Either @@ -892,7 +1022,7 @@ fn identify_algo_name_and_length( // the given length is wrong because it's not a multiple of 8. _ => return Err(LineCheckError::ImproperlyFormatted), } - } else if line_algo == ALGORITHM_OPTIONS_BLAKE2B { + } else if line_algo == AlgoKind::Blake2b { // Default length with BLAKE2b, Some(64) } else { @@ -943,26 +1073,26 @@ fn compute_and_check_digest_from_file( /// Check a digest checksum with non-algo based pre-treatment. fn process_algo_based_line( line_info: &LineInfo, - cli_algo_name: Option<&str>, + cli_algo_kind: Option, opts: ChecksumOptions, last_algo: &mut Option, ) -> Result<(), LineCheckError> { let filename_to_check = line_info.filename.as_slice(); - let (algo_name, algo_byte_len) = - identify_algo_name_and_length(line_info, cli_algo_name, last_algo)?; + let (algo_kind, algo_byte_len) = + identify_algo_name_and_length(line_info, cli_algo_kind, last_algo)?; // If the digest bitlen is known, we can check the format of the expected // checksum with it. - let digest_char_length_hint = match (algo_name.as_str(), algo_byte_len) { - (ALGORITHM_OPTIONS_BLAKE2B, Some(bytelen)) => Some(bytelen * 2), + let digest_char_length_hint = match (algo_kind, algo_byte_len) { + (AlgoKind::Blake2b, Some(bytelen)) => Some(bytelen * 2), _ => None, }; let expected_checksum = get_expected_digest_as_hex_string(line_info, digest_char_length_hint) .ok_or(LineCheckError::ImproperlyFormatted)?; - let algo = detect_algo(&algo_name, algo_byte_len)?; + let algo = detect_algo(algo_kind, algo_byte_len)?; compute_and_check_digest_from_file(filename_to_check, &expected_checksum, algo, opts) } @@ -971,7 +1101,7 @@ fn process_algo_based_line( fn process_non_algo_based_line( line_number: usize, line_info: &LineInfo, - cli_algo_name: &str, + cli_algo_kind: AlgoKind, cli_algo_length: Option, opts: ChecksumOptions, ) -> Result<(), LineCheckError> { @@ -989,24 +1119,21 @@ fn process_non_algo_based_line( // When a specific algorithm name is input, use it and use the provided // bits except when dealing with blake2b, sha2 and sha3, where we will // detect the length. - let (algo_name, algo_byte_len) = match cli_algo_name { - ALGORITHM_OPTIONS_BLAKE2B => { + let (algo_kind, algo_byte_len) = match cli_algo_kind { + AlgoKind::Blake2b => { // division by 2 converts the length of the Blake2b checksum from // hexadecimal characters to bytes, as each byte is represented by // two hexadecimal characters. - ( - ALGORITHM_OPTIONS_BLAKE2B.to_string(), - Some(expected_checksum.len() / 2), - ) + (AlgoKind::Blake2b, Some(expected_checksum.len() / 2)) } - algo @ (ALGORITHM_OPTIONS_SHA2 | ALGORITHM_OPTIONS_SHA3) => { + algo @ (AlgoKind::Sha2 | AlgoKind::Sha3) => { // multiplication by 4 to get the number of bits - (algo.to_string(), Some(expected_checksum.len() * 4)) + (algo, Some(expected_checksum.len() * 4)) } - _ => (cli_algo_name.to_lowercase(), cli_algo_length), + _ => (cli_algo_kind, cli_algo_length), }; - let algo = detect_algo(&algo_name, algo_byte_len)?; + let algo = detect_algo(algo_kind, algo_byte_len)?; compute_and_check_digest_from_file(filename_to_check, &expected_checksum, algo, opts) } @@ -1020,7 +1147,7 @@ fn process_non_algo_based_line( fn process_checksum_line( line: &OsStr, i: usize, - cli_algo_name: Option<&str>, + cli_algo_name: Option, cli_algo_length: Option, opts: ChecksumOptions, cached_line_format: &mut Option, @@ -1053,7 +1180,7 @@ fn process_checksum_line( fn process_checksum_file( filename_input: &OsStr, - cli_algo_name: Option<&str>, + cli_algo_kind: Option, cli_algo_length: Option, opts: ChecksumOptions, ) -> Result<(), FileCheckError> { @@ -1090,7 +1217,7 @@ fn process_checksum_file( let line_result = process_checksum_line( line, i, - cli_algo_name, + cli_algo_kind, cli_algo_length, opts, &mut cached_line_format, @@ -1114,12 +1241,12 @@ fn process_checksum_file( res.bad_format += 1; if opts.verbose.at_least_warning() { - let algo = if let Some(algo_name_input) = cli_algo_name { - Cow::Owned(algo_name_input.to_uppercase()) + let algo = if let Some(algo_name_input) = cli_algo_kind { + algo_name_input.to_uppercase() } else if let Some(algo) = &last_algo { - Cow::Borrowed(algo.as_str()) + algo.as_str() } else { - Cow::Borrowed("Unknown algorithm") + "Unknown algorithm" }; eprintln!( "{}: {}: {}: improperly formatted {algo} checksum line", @@ -1183,7 +1310,7 @@ fn process_checksum_file( /// Do the checksum validation (can be strict or not) pub fn perform_checksum_validation<'a, I>( files: I, - algo_name_input: Option<&str>, + algo_kind: Option, length_input: Option, opts: ChecksumOptions, ) -> UResult<()> @@ -1195,7 +1322,7 @@ where // if cksum has several input files, it will print the result for each file for filename_input in files { use FileCheckError::*; - match process_checksum_file(filename_input, algo_name_input, length_input, opts) { + match process_checksum_file(filename_input, algo_kind, length_input, opts) { Err(UError(e)) => return Err(e), Err(Failed | CantOpenChecksumFile) => failed = true, Ok(_) => (), @@ -1279,18 +1406,18 @@ pub fn calculate_blake2b_length_str(length: &str) -> UResult> { } } -pub fn validate_sha2_sha3_length(algo_name: &str, length: Option) -> UResult { +pub fn validate_sha2_sha3_length(algo_name: AlgoKind, length: Option) -> UResult { match length { Some(len @ (224 | 256 | 384 | 512)) => Ok(len), Some(len) => { show_error!("{}", ChecksumError::InvalidLength(len.to_string())); - Err(ChecksumError::InvalidLengthForSha(algo_name.to_ascii_uppercase()).into()) + Err(ChecksumError::InvalidLengthForSha(algo_name.to_uppercase().into()).into()) } - None => Err(ChecksumError::LengthRequiredForSha(algo_name.into()).into()), + None => Err(ChecksumError::LengthRequiredForSha(algo_name.to_lowercase().into()).into()), } } -pub fn sanitize_sha2_sha3_length_str(algo_name: &str, length: &str) -> UResult { +pub fn sanitize_sha2_sha3_length_str(algo_kind: AlgoKind, length: &str) -> UResult { // There is a difference in the errors sent when the length is not a number // vs. its an invalid number. // @@ -1302,7 +1429,7 @@ pub fn sanitize_sha2_sha3_length_str(algo_name: &str, length: &str) -> UResult { show_error!("{}", ChecksumError::InvalidLength(length.into())); - return Err(ChecksumError::InvalidLengthForSha(algo_name.to_ascii_uppercase()).into()); + return Err(ChecksumError::InvalidLengthForSha(algo_kind.to_uppercase().into()).into()); } Err(_) => return Err(ChecksumError::InvalidLength(length.into()).into()), }; @@ -1311,7 +1438,7 @@ pub fn sanitize_sha2_sha3_length_str(algo_name: &str, length: &str) -> UResult Date: Mon, 3 Nov 2025 23:25:22 +0100 Subject: [PATCH 072/182] checksum: Introduce `SizedAlgoKind` to improve representation and cleanup --- src/uu/cksum/src/cksum.rs | 42 ++--- src/uu/hashsum/src/hashsum.rs | 26 +-- src/uucore/src/lib/features/checksum.rs | 211 ++++++++++++++++++------ 3 files changed, 183 insertions(+), 96 deletions(-) diff --git a/src/uu/cksum/src/cksum.rs b/src/uu/cksum/src/cksum.rs index cdd9de33f..8aef74ba5 100644 --- a/src/uu/cksum/src/cksum.rs +++ b/src/uu/cksum/src/cksum.rs @@ -13,11 +13,9 @@ use std::io::{BufReader, Read, Write, stdin, stdout}; use std::iter; use std::path::Path; use uucore::checksum::{ - ALGORITHM_OPTIONS_BLAKE2B, ALGORITHM_OPTIONS_BSD, ALGORITHM_OPTIONS_CRC, - ALGORITHM_OPTIONS_CRC32B, ALGORITHM_OPTIONS_SYSV, AlgoKind, ChecksumError, ChecksumOptions, - ChecksumVerbose, HashAlgorithm, LEGACY_ALGORITHMS, SUPPORTED_ALGORITHMS, - calculate_blake2b_length_str, detect_algo, digest_reader, perform_checksum_validation, - sanitize_sha2_sha3_length_str, + AlgoKind, ChecksumError, ChecksumOptions, ChecksumVerbose, HashAlgorithm, SUPPORTED_ALGORITHMS, + SizedAlgoKind, calculate_blake2b_length_str, detect_algo, digest_reader, + perform_checksum_validation, sanitize_sha2_sha3_length_str, }; use uucore::translate; @@ -31,10 +29,9 @@ use uucore::{ }; struct Options { - algo_name: &'static str, + algo_kind: SizedAlgoKind, digest: Box, output_bits: usize, - length: Option, output_format: OutputFormat, line_ending: LineEnding, } @@ -108,16 +105,16 @@ fn print_legacy_checksum( sum: &str, size: usize, ) -> UResult<()> { - debug_assert!(LEGACY_ALGORITHMS.contains(&options.algo_name)); + debug_assert!(options.algo_kind.is_legacy()); // Print the sum - match options.algo_name { - ALGORITHM_OPTIONS_SYSV => print!( + match options.algo_kind { + SizedAlgoKind::Sysv => print!( "{} {}", sum.parse::().unwrap(), size.div_ceil(options.output_bits), ), - ALGORITHM_OPTIONS_BSD => { + SizedAlgoKind::Bsd => { // The BSD checksum output is 5 digit integer let bsd_width = 5; print!( @@ -126,7 +123,7 @@ fn print_legacy_checksum( size.div_ceil(options.output_bits), ); } - ALGORITHM_OPTIONS_CRC | ALGORITHM_OPTIONS_CRC32B => { + SizedAlgoKind::Crc | SizedAlgoKind::Crc32b => { print!("{sum} {size}"); } _ => unreachable!("Not a legacy algorithm"), @@ -143,15 +140,7 @@ fn print_legacy_checksum( fn print_tagged_checksum(options: &Options, filename: &OsStr, sum: &String) -> UResult<()> { // Print algo name and opening parenthesis. - print!( - "{} (", - match (options.algo_name, options.length) { - // Multiply the length by 8, as we want to print the length in bits. - (ALGORITHM_OPTIONS_BLAKE2B, Some(l)) => format!("BLAKE2b-{}", l * 8), - (ALGORITHM_OPTIONS_BLAKE2B, None) => "BLAKE2b".into(), - (name, _) => name.to_ascii_uppercase(), - } - ); + print!("{} (", options.algo_kind.to_tag()); // Print filename let _dropped_result = stdout().write_all(os_str_as_bytes(filename)?); @@ -235,11 +224,11 @@ where match options.output_format { OutputFormat::Raw => { - let bytes = match options.algo_name { - ALGORITHM_OPTIONS_CRC | ALGORITHM_OPTIONS_CRC32B => { + let bytes = match options.algo_kind { + SizedAlgoKind::Crc | SizedAlgoKind::Crc32b => { sum_hex.parse::().unwrap().to_be_bytes().to_vec() } - ALGORITHM_OPTIONS_SYSV | ALGORITHM_OPTIONS_BSD => { + SizedAlgoKind::Sysv | SizedAlgoKind::Bsd => { sum_hex.parse::().unwrap().to_be_bytes().to_vec() } _ => hex::decode(sum_hex).unwrap(), @@ -343,7 +332,7 @@ fn figure_out_output_format( } // Then, if the algo is legacy, takes precedence over the rest - if LEGACY_ALGORITHMS.contains(&algo.name) { + if algo.kind.is_legacy() { return OutputFormat::Legacy; } @@ -465,10 +454,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { ); let opts = Options { - algo_name: algo.name, + algo_kind: algo.kind, digest: (algo.create_fn)(), output_bits: algo.bits, - length, output_format, line_ending, }; diff --git a/src/uu/hashsum/src/hashsum.rs b/src/uu/hashsum/src/hashsum.rs index 4e0206252..dfd3caf7c 100644 --- a/src/uu/hashsum/src/hashsum.rs +++ b/src/uu/hashsum/src/hashsum.rs @@ -15,7 +15,6 @@ use std::io::{BufReader, Read, stdin}; use std::iter; use std::num::ParseIntError; use std::path::Path; -use uucore::checksum::ChecksumOptions; use uucore::checksum::ChecksumVerbose; use uucore::checksum::calculate_blake2b_length; use uucore::checksum::detect_algo; @@ -23,6 +22,7 @@ use uucore::checksum::digest_reader; use uucore::checksum::escape_filename; use uucore::checksum::perform_checksum_validation; use uucore::checksum::{AlgoKind, ChecksumError}; +use uucore::checksum::{ChecksumOptions, SizedAlgoKind}; use uucore::error::{UResult, strip_errno}; use uucore::format_usage; use uucore::sum::Digest; @@ -33,7 +33,7 @@ const NAME: &str = "hashsum"; const READ_BUFFER_SIZE: usize = 32 * 1024; struct Options<'a> { - algoname: &'static str, + algo: SizedAlgoKind, digest: Box, binary: bool, binary_name: &'a str, @@ -246,7 +246,7 @@ pub fn uumain(mut args: impl uucore::Args) -> UResult<()> { let algo = detect_algo(algo_kind, length)?; let opts = Options { - algoname: algo.name, + algo: algo.kind, digest: (algo.create_fn)(), output_bits: algo.bits, binary, @@ -549,22 +549,10 @@ where let (escaped_filename, prefix) = escape_filename(filename); if options.tag { - if options.algoname == "blake2b" { - if options.digest.output_bits() == 512 { - println!("BLAKE2b ({escaped_filename}) = {sum}"); - } else { - // special case for BLAKE2b with non-default output length - println!( - "BLAKE2b-{} ({escaped_filename}) = {sum}", - options.digest.output_bits() - ); - } - } else { - println!( - "{prefix}{} ({escaped_filename}) = {sum}", - options.algoname.to_ascii_uppercase() - ); - } + println!( + "{prefix}{} ({escaped_filename}) = {sum}", + options.algo.to_tag() + ); } else if options.nonames { println!("{sum}"); } else if options.zero { diff --git a/src/uucore/src/lib/features/checksum.rs b/src/uucore/src/lib/features/checksum.rs index e71d8135c..3437a0a6e 100644 --- a/src/uucore/src/lib/features/checksum.rs +++ b/src/uucore/src/lib/features/checksum.rs @@ -72,13 +72,6 @@ pub const SUPPORTED_ALGORITHMS: [&str; 17] = [ ALGORITHM_OPTIONS_SHAKE256, ]; -pub const LEGACY_ALGORITHMS: [&str; 4] = [ - ALGORITHM_OPTIONS_SYSV, - ALGORITHM_OPTIONS_BSD, - ALGORITHM_OPTIONS_CRC, - ALGORITHM_OPTIONS_CRC32B, -]; - /// Represents an algorithm kind. In some cases, it is not sufficient by itself /// to know which algorithm to use exactly, because it lacks a digest length, /// which is why [`SizedAlgoKind`] exists. @@ -212,8 +205,127 @@ impl AlgoKind { } } +/// Holds a length for a SHA2 of SHA3 algorithm kind. +#[derive(Debug, Clone, Copy)] +pub enum ShaLength { + Len224, + Len256, + Len384, + Len512, +} + +impl ShaLength { + pub fn as_usize(self) -> usize { + match self { + Self::Len224 => 224, + Self::Len256 => 256, + Self::Len384 => 384, + Self::Len512 => 512, + } + } +} + +impl TryFrom for ShaLength { + type Error = ChecksumError; + + fn try_from(value: usize) -> Result { + use ShaLength::*; + match value { + 224 => Ok(Len224), + 256 => Ok(Len256), + 384 => Ok(Len384), + 512 => Ok(Len512), + _ => Err(ChecksumError::InvalidLengthForSha(value.to_string())), + } + } +} + +/// Represents an actual determined algorithm. +#[derive(Debug, Clone, Copy)] +pub enum SizedAlgoKind { + Sysv, + Bsd, + Crc, + Crc32b, + Md5, + Sm3, + Sha1, + Blake3, + Sha2(ShaLength), + Sha3(ShaLength), + Blake2b(Option), + Shake128(usize), + Shake256(usize), +} + +impl SizedAlgoKind { + pub fn from_unsized(kind: AlgoKind, length: Option) -> UResult { + use AlgoKind as ak; + match (kind, length) { + ( + ak::Sysv + | ak::Bsd + | ak::Crc + | ak::Crc32b + | ak::Md5 + | ak::Sm3 + | ak::Sha1 + | ak::Blake3 + | ak::Sha224 + | ak::Sha256 + | ak::Sha384 + | ak::Sha512, + Some(_), + ) => Err(ChecksumError::LengthOnlyForBlake2bSha2Sha3.into()), + + (ak::Sysv, _) => Ok(Self::Sysv), + (ak::Bsd, _) => Ok(Self::Bsd), + (ak::Crc, _) => Ok(Self::Crc), + (ak::Crc32b, _) => Ok(Self::Crc32b), + (ak::Md5, _) => Ok(Self::Md5), + (ak::Sm3, _) => Ok(Self::Sm3), + (ak::Sha1, _) => Ok(Self::Sha1), + (ak::Blake3, _) => Ok(Self::Blake3), + + (ak::Shake128, Some(l)) => Ok(Self::Shake128(l)), + (ak::Shake256, Some(l)) => Ok(Self::Shake256(l)), + (ak::Sha2, Some(l)) => Ok(Self::Sha2(ShaLength::try_from(l)?)), + (ak::Sha3, Some(l)) => Ok(Self::Sha3(ShaLength::try_from(l)?)), + (ak::Blake2b, Some(l)) => Ok(Self::Blake2b(calculate_blake2b_length(l)?)), + + (ak::Sha224, None) => Ok(Self::Sha2(ShaLength::Len224)), + (ak::Sha256, None) => Ok(Self::Sha2(ShaLength::Len256)), + (ak::Sha384, None) => Ok(Self::Sha2(ShaLength::Len384)), + (ak::Sha512, None) => Ok(Self::Sha2(ShaLength::Len512)), + (_, None) => Err(ChecksumError::LengthRequired(kind.to_uppercase().into()).into()), + } + } + + pub fn to_tag(&self) -> String { + use SizedAlgoKind::*; + match self { + Md5 => "MD5".into(), + Sm3 => "SM3".into(), + Sha1 => "SHA1".into(), + Blake3 => "BLAKE3".into(), + Sha2(len) => format!("SHA{}", len.as_usize()), + Sha3(len) => format!("SHA3-{}", len.as_usize()), + Blake2b(Some(len)) => format!("BLAKE2b-{}", len * 8), + Blake2b(None) => "BLAKE2b".into(), + Shake128(_) => "SHAKE128".into(), + Shake256(_) => "SHAKE256".into(), + Sysv | Bsd | Crc | Crc32b => panic!("Should not be used for tagging"), + } + } + + pub fn is_legacy(&self) -> bool { + use SizedAlgoKind::*; + matches!(self, Sysv | Bsd | Crc | Crc32b) + } +} + pub struct HashAlgorithm { - pub name: &'static str, + pub kind: SizedAlgoKind, pub create_fn: Box Box>, pub bits: usize, } @@ -399,57 +511,53 @@ impl UError for ChecksumError { /// /// Returns a `UResult` with an `HashAlgorithm` or an `Err` if an unsupported /// output size is provided. -pub fn create_sha3(bits: usize) -> UResult { - match bits { - 224 => Ok(HashAlgorithm { - name: "SHA3-224", +pub fn create_sha3(len: ShaLength) -> UResult { + match len { + ShaLength::Len224 => Ok(HashAlgorithm { + kind: SizedAlgoKind::Sha3(ShaLength::Len224), create_fn: Box::new(|| Box::new(Sha3_224::new())), bits: 224, }), - 256 => Ok(HashAlgorithm { - name: "SHA3-256", + ShaLength::Len256 => Ok(HashAlgorithm { + kind: SizedAlgoKind::Sha3(ShaLength::Len256), create_fn: Box::new(|| Box::new(Sha3_256::new())), bits: 256, }), - 384 => Ok(HashAlgorithm { - name: "SHA3-384", + ShaLength::Len384 => Ok(HashAlgorithm { + kind: SizedAlgoKind::Sha3(ShaLength::Len384), create_fn: Box::new(|| Box::new(Sha3_384::new())), bits: 384, }), - 512 => Ok(HashAlgorithm { - name: "SHA3-512", + ShaLength::Len512 => Ok(HashAlgorithm { + kind: SizedAlgoKind::Sha3(ShaLength::Len512), create_fn: Box::new(|| Box::new(Sha3_512::new())), bits: 512, }), - - _ => Err(ChecksumError::InvalidLengthForSha("SHA3".into()).into()), } } -pub fn create_sha2(bits: usize) -> UResult { - match bits { - 224 => Ok(HashAlgorithm { - name: "SHA224", +pub fn create_sha2(len: ShaLength) -> UResult { + match len { + ShaLength::Len224 => Ok(HashAlgorithm { + kind: SizedAlgoKind::Sha2(ShaLength::Len224), create_fn: Box::new(|| Box::new(Sha224::new())), bits: 224, }), - 256 => Ok(HashAlgorithm { - name: "SHA256", + ShaLength::Len256 => Ok(HashAlgorithm { + kind: SizedAlgoKind::Sha2(ShaLength::Len256), create_fn: Box::new(|| Box::new(Sha256::new())), bits: 256, }), - 384 => Ok(HashAlgorithm { - name: "SHA384", + ShaLength::Len384 => Ok(HashAlgorithm { + kind: SizedAlgoKind::Sha2(ShaLength::Len384), create_fn: Box::new(|| Box::new(Sha384::new())), bits: 384, }), - 512 => Ok(HashAlgorithm { - name: "SHA512", + ShaLength::Len512 => Ok(HashAlgorithm { + kind: SizedAlgoKind::Sha2(ShaLength::Len512), create_fn: Box::new(|| Box::new(Sha512::new())), bits: 512, }), - - _ => Err(ChecksumError::InvalidLengthForSha("SHA2".into()).into()), } } @@ -540,63 +648,63 @@ fn print_file_report( pub fn detect_algo(algo: AlgoKind, length: Option) -> UResult { match algo { AlgoKind::Sysv => Ok(HashAlgorithm { - name: ALGORITHM_OPTIONS_SYSV, + kind: SizedAlgoKind::Sysv, create_fn: Box::new(|| Box::new(SysV::new())), bits: 512, }), AlgoKind::Bsd => Ok(HashAlgorithm { - name: ALGORITHM_OPTIONS_BSD, + kind: SizedAlgoKind::Bsd, create_fn: Box::new(|| Box::new(Bsd::new())), bits: 1024, }), AlgoKind::Crc => Ok(HashAlgorithm { - name: ALGORITHM_OPTIONS_CRC, + kind: SizedAlgoKind::Crc, create_fn: Box::new(|| Box::new(Crc::new())), bits: 256, }), AlgoKind::Crc32b => Ok(HashAlgorithm { - name: ALGORITHM_OPTIONS_CRC32B, + kind: SizedAlgoKind::Crc32b, create_fn: Box::new(|| Box::new(CRC32B::new())), bits: 32, }), AlgoKind::Md5 => Ok(HashAlgorithm { - name: ALGORITHM_OPTIONS_MD5, + kind: SizedAlgoKind::Md5, create_fn: Box::new(|| Box::new(Md5::new())), bits: 128, }), AlgoKind::Sha1 => Ok(HashAlgorithm { - name: ALGORITHM_OPTIONS_SHA1, + kind: SizedAlgoKind::Sha1, create_fn: Box::new(|| Box::new(Sha1::new())), bits: 160, }), - AlgoKind::Sha224 => Ok(create_sha2(224)?), - AlgoKind::Sha256 => Ok(create_sha2(256)?), - AlgoKind::Sha384 => Ok(create_sha2(384)?), - AlgoKind::Sha512 => Ok(create_sha2(512)?), + AlgoKind::Sha224 => Ok(create_sha2(ShaLength::Len224)?), + AlgoKind::Sha256 => Ok(create_sha2(ShaLength::Len256)?), + AlgoKind::Sha384 => Ok(create_sha2(ShaLength::Len384)?), + AlgoKind::Sha512 => Ok(create_sha2(ShaLength::Len512)?), AlgoKind::Blake2b => { // Set default length to 512 if None let bits = length.unwrap_or(512); if bits == 512 { Ok(HashAlgorithm { - name: ALGORITHM_OPTIONS_BLAKE2B, + kind: SizedAlgoKind::Blake2b(None), create_fn: Box::new(move || Box::new(Blake2b::new())), bits: 512, }) } else { Ok(HashAlgorithm { - name: ALGORITHM_OPTIONS_BLAKE2B, + kind: SizedAlgoKind::Blake2b(Some(bits)), create_fn: Box::new(move || Box::new(Blake2b::with_output_bytes(bits))), bits, }) } } AlgoKind::Blake3 => Ok(HashAlgorithm { - name: ALGORITHM_OPTIONS_BLAKE3, + kind: SizedAlgoKind::Blake3, create_fn: Box::new(|| Box::new(Blake3::new())), bits: 256, }), AlgoKind::Sm3 => Ok(HashAlgorithm { - name: ALGORITHM_OPTIONS_SM3, + kind: SizedAlgoKind::Sm3, create_fn: Box::new(|| Box::new(Sm3::new())), bits: 512, }), @@ -605,7 +713,7 @@ pub fn detect_algo(algo: AlgoKind, length: Option) -> UResult) -> UResult UResult> { } } -pub fn validate_sha2_sha3_length(algo_name: AlgoKind, length: Option) -> UResult { +pub fn validate_sha2_sha3_length(algo_name: AlgoKind, length: Option) -> UResult { match length { - Some(len @ (224 | 256 | 384 | 512)) => Ok(len), + Some(224) => Ok(ShaLength::Len224), + Some(256) => Ok(ShaLength::Len256), + Some(384) => Ok(ShaLength::Len384), + Some(512) => Ok(ShaLength::Len512), Some(len) => { show_error!("{}", ChecksumError::InvalidLength(len.to_string())); Err(ChecksumError::InvalidLengthForSha(algo_name.to_uppercase().into()).into()) From 923baae8536ed14350a2bad7f924b95e11a7ae18 Mon Sep 17 00:00:00 2001 From: Dorian Peron Date: Tue, 4 Nov 2025 00:46:10 +0100 Subject: [PATCH 073/182] checksum: Always get bit length from the SizedAlgoKind --- src/uu/cksum/src/cksum.rs | 18 ++++---- src/uu/hashsum/src/hashsum.rs | 6 +-- src/uucore/src/lib/features/checksum.rs | 58 ++++++++++++------------- 3 files changed, 39 insertions(+), 43 deletions(-) diff --git a/src/uu/cksum/src/cksum.rs b/src/uu/cksum/src/cksum.rs index 8aef74ba5..46ccad41e 100644 --- a/src/uu/cksum/src/cksum.rs +++ b/src/uu/cksum/src/cksum.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) fname, algo +// spell-checker:ignore (ToDO) fname, algo, bitlen use clap::builder::ValueParser; use clap::{Arg, ArgAction, Command}; @@ -31,7 +31,6 @@ use uucore::{ struct Options { algo_kind: SizedAlgoKind, digest: Box, - output_bits: usize, output_format: OutputFormat, line_ending: LineEnding, } @@ -112,7 +111,7 @@ fn print_legacy_checksum( SizedAlgoKind::Sysv => print!( "{} {}", sum.parse::().unwrap(), - size.div_ceil(options.output_bits), + size.div_ceil(options.algo_kind.bitlen()), ), SizedAlgoKind::Bsd => { // The BSD checksum output is 5 digit integer @@ -120,7 +119,7 @@ fn print_legacy_checksum( print!( "{:0bsd_width$} {:bsd_width$}", sum.parse::().unwrap(), - size.div_ceil(options.output_bits), + size.div_ceil(options.algo_kind.bitlen()), ); } SizedAlgoKind::Crc | SizedAlgoKind::Crc32b => { @@ -209,9 +208,13 @@ where Box::new(file_buf) as Box }); - let (sum_hex, sz) = - digest_reader(&mut options.digest, &mut file, false, options.output_bits) - .map_err_context(|| translate!("cksum-error-failed-to-read-input"))?; + let (sum_hex, sz) = digest_reader( + &mut options.digest, + &mut file, + false, + options.algo_kind.bitlen(), + ) + .map_err_context(|| translate!("cksum-error-failed-to-read-input"))?; // Encodes the sum if df is Base64, leaves as-is otherwise. let encode_sum = |sum: String, df: DigestFormat| { @@ -456,7 +459,6 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let opts = Options { algo_kind: algo.kind, digest: (algo.create_fn)(), - output_bits: algo.bits, output_format, line_ending, }; diff --git a/src/uu/hashsum/src/hashsum.rs b/src/uu/hashsum/src/hashsum.rs index dfd3caf7c..6eebf651d 100644 --- a/src/uu/hashsum/src/hashsum.rs +++ b/src/uu/hashsum/src/hashsum.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) algo, algoname, regexes, nread, nonames +// spell-checker:ignore (ToDO) algo, algoname, bitlen, regexes, nread, nonames use clap::ArgAction; use clap::builder::ValueParser; @@ -44,7 +44,6 @@ struct Options<'a> { //quiet: bool, //strict: bool, //warn: bool, - output_bits: usize, zero: bool, //ignore_missing: bool, } @@ -248,7 +247,6 @@ pub fn uumain(mut args: impl uucore::Args) -> UResult<()> { let opts = Options { algo: algo.kind, digest: (algo.create_fn)(), - output_bits: algo.bits, binary, binary_name: &binary_name, tag: matches.get_flag("tag"), @@ -532,7 +530,7 @@ where &mut options.digest, &mut file, options.binary, - options.output_bits, + options.algo.bitlen(), ) { Ok((sum, _)) => sum, Err(e) => { diff --git a/src/uucore/src/lib/features/checksum.rs b/src/uucore/src/lib/features/checksum.rs index 3437a0a6e..b008dd986 100644 --- a/src/uucore/src/lib/features/checksum.rs +++ b/src/uucore/src/lib/features/checksum.rs @@ -301,7 +301,7 @@ impl SizedAlgoKind { } } - pub fn to_tag(&self) -> String { + pub fn to_tag(self) -> String { use SizedAlgoKind::*; match self { Md5 => "MD5".into(), @@ -318,6 +318,24 @@ impl SizedAlgoKind { } } + pub fn bitlen(&self) -> usize { + use SizedAlgoKind::*; + match self { + Sysv => 512, + Bsd => 1024, + Crc => 256, + Crc32b => 32, + Md5 => 128, + Sm3 => 512, + Sha1 => 160, + Blake3 => 256, + Sha2(len) => len.as_usize(), + Sha3(len) => len.as_usize(), + Blake2b(len) => len.unwrap_or(512), + Shake128(len) => *len, + Shake256(len) => *len, + } + } pub fn is_legacy(&self) -> bool { use SizedAlgoKind::*; matches!(self, Sysv | Bsd | Crc | Crc32b) @@ -327,7 +345,6 @@ impl SizedAlgoKind { pub struct HashAlgorithm { pub kind: SizedAlgoKind, pub create_fn: Box Box>, - pub bits: usize, } /// This structure holds the count of checksum test lines' outcomes. @@ -516,22 +533,18 @@ pub fn create_sha3(len: ShaLength) -> UResult { ShaLength::Len224 => Ok(HashAlgorithm { kind: SizedAlgoKind::Sha3(ShaLength::Len224), create_fn: Box::new(|| Box::new(Sha3_224::new())), - bits: 224, }), ShaLength::Len256 => Ok(HashAlgorithm { kind: SizedAlgoKind::Sha3(ShaLength::Len256), create_fn: Box::new(|| Box::new(Sha3_256::new())), - bits: 256, }), ShaLength::Len384 => Ok(HashAlgorithm { kind: SizedAlgoKind::Sha3(ShaLength::Len384), create_fn: Box::new(|| Box::new(Sha3_384::new())), - bits: 384, }), ShaLength::Len512 => Ok(HashAlgorithm { kind: SizedAlgoKind::Sha3(ShaLength::Len512), create_fn: Box::new(|| Box::new(Sha3_512::new())), - bits: 512, }), } } @@ -541,22 +554,18 @@ pub fn create_sha2(len: ShaLength) -> UResult { ShaLength::Len224 => Ok(HashAlgorithm { kind: SizedAlgoKind::Sha2(ShaLength::Len224), create_fn: Box::new(|| Box::new(Sha224::new())), - bits: 224, }), ShaLength::Len256 => Ok(HashAlgorithm { kind: SizedAlgoKind::Sha2(ShaLength::Len256), create_fn: Box::new(|| Box::new(Sha256::new())), - bits: 256, }), ShaLength::Len384 => Ok(HashAlgorithm { kind: SizedAlgoKind::Sha2(ShaLength::Len384), create_fn: Box::new(|| Box::new(Sha384::new())), - bits: 384, }), ShaLength::Len512 => Ok(HashAlgorithm { kind: SizedAlgoKind::Sha2(ShaLength::Len512), create_fn: Box::new(|| Box::new(Sha512::new())), - bits: 512, }), } } @@ -650,32 +659,26 @@ pub fn detect_algo(algo: AlgoKind, length: Option) -> UResult Ok(HashAlgorithm { kind: SizedAlgoKind::Sysv, create_fn: Box::new(|| Box::new(SysV::new())), - bits: 512, }), AlgoKind::Bsd => Ok(HashAlgorithm { kind: SizedAlgoKind::Bsd, create_fn: Box::new(|| Box::new(Bsd::new())), - bits: 1024, }), AlgoKind::Crc => Ok(HashAlgorithm { kind: SizedAlgoKind::Crc, create_fn: Box::new(|| Box::new(Crc::new())), - bits: 256, }), AlgoKind::Crc32b => Ok(HashAlgorithm { kind: SizedAlgoKind::Crc32b, create_fn: Box::new(|| Box::new(CRC32B::new())), - bits: 32, }), AlgoKind::Md5 => Ok(HashAlgorithm { kind: SizedAlgoKind::Md5, create_fn: Box::new(|| Box::new(Md5::new())), - bits: 128, }), AlgoKind::Sha1 => Ok(HashAlgorithm { kind: SizedAlgoKind::Sha1, create_fn: Box::new(|| Box::new(Sha1::new())), - bits: 160, }), AlgoKind::Sha224 => Ok(create_sha2(ShaLength::Len224)?), AlgoKind::Sha256 => Ok(create_sha2(ShaLength::Len256)?), @@ -688,25 +691,21 @@ pub fn detect_algo(algo: AlgoKind, length: Option) -> UResult Ok(HashAlgorithm { kind: SizedAlgoKind::Blake3, create_fn: Box::new(|| Box::new(Blake3::new())), - bits: 256, }), AlgoKind::Sm3 => Ok(HashAlgorithm { kind: SizedAlgoKind::Sm3, create_fn: Box::new(|| Box::new(Sm3::new())), - bits: 512, }), AlgoKind::Shake128 => { let bits = length.ok_or(ChecksumError::LengthRequired( @@ -715,7 +714,6 @@ pub fn detect_algo(algo: AlgoKind, length: Option) -> UResult { @@ -725,7 +723,6 @@ pub fn detect_algo(algo: AlgoKind, length: Option) -> UResult { @@ -735,13 +732,7 @@ pub fn detect_algo(algo: AlgoKind, length: Option) -> UResult { let len = validate_sha2_sha3_length(algo, length)?; create_sha3(len) - } // TODO: `hashsum` specific, to remove once hashsum is removed. - // algo @ ("sha3-224" | "sha3-256" | "sha3-384" | "sha3-512") => { - // let bits: usize = algo.strip_prefix("sha3-").unwrap().parse().unwrap(); - // create_sha3(bits) - // } - - // algo => Err(ChecksumError::UnknownAlgorithm(algo.into()).into()), + } } } @@ -1158,8 +1149,13 @@ fn compute_and_check_digest_from_file( // Read the file and calculate the checksum let create_fn = &mut algo.create_fn; let mut digest = create_fn(); - let (calculated_checksum, _) = - digest_reader(&mut digest, &mut file_reader, opts.binary, algo.bits).unwrap(); + let (calculated_checksum, _) = digest_reader( + &mut digest, + &mut file_reader, + opts.binary, + algo.kind.bitlen(), + ) + .unwrap(); // Do the checksum validation let checksum_correct = expected_checksum == calculated_checksum; From 68dc6fd1470b3515890b8fc7c5950042f6b5f731 Mon Sep 17 00:00:00 2001 From: Dorian Peron Date: Tue, 4 Nov 2025 02:18:36 +0100 Subject: [PATCH 074/182] checksum: Get rid of detect_algo and child functions --- src/uu/cksum/src/cksum.rs | 18 +- src/uu/hashsum/src/hashsum.rs | 7 +- src/uucore/src/lib/features/checksum.rs | 302 ++++-------------------- 3 files changed, 63 insertions(+), 264 deletions(-) diff --git a/src/uu/cksum/src/cksum.rs b/src/uu/cksum/src/cksum.rs index 46ccad41e..b80139f67 100644 --- a/src/uu/cksum/src/cksum.rs +++ b/src/uu/cksum/src/cksum.rs @@ -13,9 +13,9 @@ use std::io::{BufReader, Read, Write, stdin, stdout}; use std::iter; use std::path::Path; use uucore::checksum::{ - AlgoKind, ChecksumError, ChecksumOptions, ChecksumVerbose, HashAlgorithm, SUPPORTED_ALGORITHMS, - SizedAlgoKind, calculate_blake2b_length_str, detect_algo, digest_reader, - perform_checksum_validation, sanitize_sha2_sha3_length_str, + AlgoKind, ChecksumError, ChecksumOptions, ChecksumVerbose, SUPPORTED_ALGORITHMS, SizedAlgoKind, + calculate_blake2b_length_str, digest_reader, perform_checksum_validation, + sanitize_sha2_sha3_length_str, }; use uucore::translate; @@ -323,7 +323,7 @@ fn handle_tag_text_binary_flags>( /// Use already-processed arguments to decide the output format. fn figure_out_output_format( - algo: &HashAlgorithm, + algo: SizedAlgoKind, tag: bool, binary: bool, raw: bool, @@ -335,7 +335,7 @@ fn figure_out_output_format( } // Then, if the algo is legacy, takes precedence over the rest - if algo.kind.is_legacy() { + if algo.is_legacy() { return OutputFormat::Legacy; } @@ -445,11 +445,11 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let (tag, binary) = handle_tag_text_binary_flags(std::env::args_os())?; - let algo = detect_algo(algo_kind, length)?; + let algo = SizedAlgoKind::from_unsized(algo_kind, length)?; let line_ending = LineEnding::from_zero_flag(matches.get_flag(options::ZERO)); let output_format = figure_out_output_format( - &algo, + algo, tag, binary, matches.get_flag(options::RAW), @@ -457,8 +457,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { ); let opts = Options { - algo_kind: algo.kind, - digest: (algo.create_fn)(), + algo_kind: algo, + digest: algo.create_digest(), output_format, line_ending, }; diff --git a/src/uu/hashsum/src/hashsum.rs b/src/uu/hashsum/src/hashsum.rs index 6eebf651d..c9f600c32 100644 --- a/src/uu/hashsum/src/hashsum.rs +++ b/src/uu/hashsum/src/hashsum.rs @@ -17,7 +17,6 @@ use std::num::ParseIntError; use std::path::Path; use uucore::checksum::ChecksumVerbose; use uucore::checksum::calculate_blake2b_length; -use uucore::checksum::detect_algo; use uucore::checksum::digest_reader; use uucore::checksum::escape_filename; use uucore::checksum::perform_checksum_validation; @@ -242,11 +241,11 @@ pub fn uumain(mut args: impl uucore::Args) -> UResult<()> { .unwrap_or(&false); let zero = matches.get_flag("zero"); - let algo = detect_algo(algo_kind, length)?; + let algo = SizedAlgoKind::from_unsized(algo_kind, length)?; let opts = Options { - algo: algo.kind, - digest: (algo.create_fn)(), + algo, + digest: algo.create_digest(), binary, binary_name: &binary_name, tag: matches.get_flag("tag"), diff --git a/src/uucore/src/lib/features/checksum.rs b/src/uucore/src/lib/features/checksum.rs index b008dd986..aed5b9111 100644 --- a/src/uucore/src/lib/features/checksum.rs +++ b/src/uucore/src/lib/features/checksum.rs @@ -253,15 +253,16 @@ pub enum SizedAlgoKind { Blake3, Sha2(ShaLength), Sha3(ShaLength), + // Note: we store Blake2b's length as BYTES. Blake2b(Option), Shake128(usize), Shake256(usize), } impl SizedAlgoKind { - pub fn from_unsized(kind: AlgoKind, length: Option) -> UResult { + pub fn from_unsized(kind: AlgoKind, byte_length: Option) -> UResult { use AlgoKind as ak; - match (kind, length) { + match (kind, byte_length) { ( ak::Sysv | ak::Bsd @@ -291,7 +292,13 @@ impl SizedAlgoKind { (ak::Shake256, Some(l)) => Ok(Self::Shake256(l)), (ak::Sha2, Some(l)) => Ok(Self::Sha2(ShaLength::try_from(l)?)), (ak::Sha3, Some(l)) => Ok(Self::Sha3(ShaLength::try_from(l)?)), - (ak::Blake2b, Some(l)) => Ok(Self::Blake2b(calculate_blake2b_length(l)?)), + (algo @ (ak::Sha2 | ak::Sha3), None) => { + Err(ChecksumError::LengthRequiredForSha(algo.to_lowercase().into()).into()) + } + // [`calculate_blake2b_length`] expects a length in bits but we + // have a length in bytes. + (ak::Blake2b, Some(l)) => Ok(Self::Blake2b(calculate_blake2b_length(8 * l)?)), + (ak::Blake2b, None) => Ok(Self::Blake2b(None)), (ak::Sha224, None) => Ok(Self::Sha2(ShaLength::Len224)), (ak::Sha256, None) => Ok(Self::Sha2(ShaLength::Len256)), @@ -310,7 +317,7 @@ impl SizedAlgoKind { Blake3 => "BLAKE3".into(), Sha2(len) => format!("SHA{}", len.as_usize()), Sha3(len) => format!("SHA3-{}", len.as_usize()), - Blake2b(Some(len)) => format!("BLAKE2b-{}", len * 8), + Blake2b(Some(byte_len)) => format!("BLAKE2b-{}", byte_len * 8), Blake2b(None) => "BLAKE2b".into(), Shake128(_) => "SHAKE128".into(), Shake256(_) => "SHAKE256".into(), @@ -318,6 +325,32 @@ impl SizedAlgoKind { } } + pub fn create_digest(&self) -> Box { + use ShaLength::*; + match self { + Self::Sysv => Box::new(SysV::new()), + Self::Bsd => Box::new(Bsd::new()), + Self::Crc => Box::new(Crc::new()), + Self::Crc32b => Box::new(CRC32B::new()), + Self::Md5 => Box::new(Md5::new()), + Self::Sm3 => Box::new(Sm3::new()), + Self::Sha1 => Box::new(Sha1::new()), + Self::Blake3 => Box::new(Blake3::new()), + Self::Sha2(Len224) => Box::new(Sha224::new()), + Self::Sha2(Len256) => Box::new(Sha256::new()), + Self::Sha2(Len384) => Box::new(Sha384::new()), + Self::Sha2(Len512) => Box::new(Sha512::new()), + Self::Sha3(Len224) => Box::new(Sha3_224::new()), + Self::Sha3(Len256) => Box::new(Sha3_256::new()), + Self::Sha3(Len384) => Box::new(Sha3_384::new()), + Self::Sha3(Len512) => Box::new(Sha3_512::new()), + Self::Blake2b(Some(byte_len)) => Box::new(Blake2b::with_output_bytes(*byte_len)), + Self::Blake2b(None) => Box::new(Blake2b::new()), + Self::Shake128(_) => Box::new(Shake128::new()), + Self::Shake256(_) => Box::new(Shake256::new()), + } + } + pub fn bitlen(&self) -> usize { use SizedAlgoKind::*; match self { @@ -342,11 +375,6 @@ impl SizedAlgoKind { } } -pub struct HashAlgorithm { - pub kind: SizedAlgoKind, - pub create_fn: Box Box>, -} - /// This structure holds the count of checksum test lines' outcomes. #[derive(Default)] struct ChecksumResult { @@ -522,54 +550,6 @@ impl UError for ChecksumError { } } -/// Creates a SHA3 hasher instance based on the specified bits argument. -/// -/// # Returns -/// -/// Returns a `UResult` with an `HashAlgorithm` or an `Err` if an unsupported -/// output size is provided. -pub fn create_sha3(len: ShaLength) -> UResult { - match len { - ShaLength::Len224 => Ok(HashAlgorithm { - kind: SizedAlgoKind::Sha3(ShaLength::Len224), - create_fn: Box::new(|| Box::new(Sha3_224::new())), - }), - ShaLength::Len256 => Ok(HashAlgorithm { - kind: SizedAlgoKind::Sha3(ShaLength::Len256), - create_fn: Box::new(|| Box::new(Sha3_256::new())), - }), - ShaLength::Len384 => Ok(HashAlgorithm { - kind: SizedAlgoKind::Sha3(ShaLength::Len384), - create_fn: Box::new(|| Box::new(Sha3_384::new())), - }), - ShaLength::Len512 => Ok(HashAlgorithm { - kind: SizedAlgoKind::Sha3(ShaLength::Len512), - create_fn: Box::new(|| Box::new(Sha3_512::new())), - }), - } -} - -pub fn create_sha2(len: ShaLength) -> UResult { - match len { - ShaLength::Len224 => Ok(HashAlgorithm { - kind: SizedAlgoKind::Sha2(ShaLength::Len224), - create_fn: Box::new(|| Box::new(Sha224::new())), - }), - ShaLength::Len256 => Ok(HashAlgorithm { - kind: SizedAlgoKind::Sha2(ShaLength::Len256), - create_fn: Box::new(|| Box::new(Sha256::new())), - }), - ShaLength::Len384 => Ok(HashAlgorithm { - kind: SizedAlgoKind::Sha2(ShaLength::Len384), - create_fn: Box::new(|| Box::new(Sha384::new())), - }), - ShaLength::Len512 => Ok(HashAlgorithm { - kind: SizedAlgoKind::Sha2(ShaLength::Len512), - create_fn: Box::new(|| Box::new(Sha512::new())), - }), - } -} - #[allow(clippy::comparison_chain)] fn print_cksum_report(res: &ChecksumResult) { if res.bad_format == 1 { @@ -654,88 +634,6 @@ fn print_file_report( } } -pub fn detect_algo(algo: AlgoKind, length: Option) -> UResult { - match algo { - AlgoKind::Sysv => Ok(HashAlgorithm { - kind: SizedAlgoKind::Sysv, - create_fn: Box::new(|| Box::new(SysV::new())), - }), - AlgoKind::Bsd => Ok(HashAlgorithm { - kind: SizedAlgoKind::Bsd, - create_fn: Box::new(|| Box::new(Bsd::new())), - }), - AlgoKind::Crc => Ok(HashAlgorithm { - kind: SizedAlgoKind::Crc, - create_fn: Box::new(|| Box::new(Crc::new())), - }), - AlgoKind::Crc32b => Ok(HashAlgorithm { - kind: SizedAlgoKind::Crc32b, - create_fn: Box::new(|| Box::new(CRC32B::new())), - }), - AlgoKind::Md5 => Ok(HashAlgorithm { - kind: SizedAlgoKind::Md5, - create_fn: Box::new(|| Box::new(Md5::new())), - }), - AlgoKind::Sha1 => Ok(HashAlgorithm { - kind: SizedAlgoKind::Sha1, - create_fn: Box::new(|| Box::new(Sha1::new())), - }), - AlgoKind::Sha224 => Ok(create_sha2(ShaLength::Len224)?), - AlgoKind::Sha256 => Ok(create_sha2(ShaLength::Len256)?), - AlgoKind::Sha384 => Ok(create_sha2(ShaLength::Len384)?), - AlgoKind::Sha512 => Ok(create_sha2(ShaLength::Len512)?), - AlgoKind::Blake2b => { - // Set default length to 512 if None - let bits = length.unwrap_or(512); - if bits == 512 { - Ok(HashAlgorithm { - kind: SizedAlgoKind::Blake2b(None), - create_fn: Box::new(move || Box::new(Blake2b::new())), - }) - } else { - Ok(HashAlgorithm { - kind: SizedAlgoKind::Blake2b(Some(bits)), - create_fn: Box::new(move || Box::new(Blake2b::with_output_bytes(bits))), - }) - } - } - AlgoKind::Blake3 => Ok(HashAlgorithm { - kind: SizedAlgoKind::Blake3, - create_fn: Box::new(|| Box::new(Blake3::new())), - }), - AlgoKind::Sm3 => Ok(HashAlgorithm { - kind: SizedAlgoKind::Sm3, - create_fn: Box::new(|| Box::new(Sm3::new())), - }), - AlgoKind::Shake128 => { - let bits = length.ok_or(ChecksumError::LengthRequired( - algo.to_uppercase().to_string(), - ))?; - Ok(HashAlgorithm { - kind: SizedAlgoKind::Shake128(bits), - create_fn: Box::new(|| Box::new(Shake128::new())), - }) - } - AlgoKind::Shake256 => { - let bits = length.ok_or(ChecksumError::LengthRequired( - algo.to_uppercase().to_string(), - ))?; - Ok(HashAlgorithm { - kind: SizedAlgoKind::Shake256(bits), - create_fn: Box::new(|| Box::new(Shake256::new())), - }) - } - AlgoKind::Sha2 => { - let len = validate_sha2_sha3_length(algo, length)?; - create_sha2(len) - } - AlgoKind::Sha3 => { - let len = validate_sha2_sha3_length(algo, length)?; - create_sha3(len) - } - } -} - #[derive(Debug, PartialEq, Eq, Clone, Copy)] enum LineFormat { AlgoBased, @@ -1136,7 +1034,7 @@ fn identify_algo_name_and_length( fn compute_and_check_digest_from_file( filename: &[u8], expected_checksum: &str, - mut algo: HashAlgorithm, + algo: SizedAlgoKind, opts: ChecksumOptions, ) -> Result<(), LineCheckError> { let (filename_to_check_unescaped, prefix) = unescape_filename(filename); @@ -1147,15 +1045,9 @@ fn compute_and_check_digest_from_file( let mut file_reader = BufReader::new(file_to_check); // Read the file and calculate the checksum - let create_fn = &mut algo.create_fn; - let mut digest = create_fn(); - let (calculated_checksum, _) = digest_reader( - &mut digest, - &mut file_reader, - opts.binary, - algo.kind.bitlen(), - ) - .unwrap(); + let mut digest = algo.create_digest(); + let (calculated_checksum, _) = + digest_reader(&mut digest, &mut file_reader, opts.binary, algo.bitlen()).unwrap(); // Do the checksum validation let checksum_correct = expected_checksum == calculated_checksum; @@ -1196,7 +1088,7 @@ fn process_algo_based_line( let expected_checksum = get_expected_digest_as_hex_string(line_info, digest_char_length_hint) .ok_or(LineCheckError::ImproperlyFormatted)?; - let algo = detect_algo(algo_kind, algo_byte_len)?; + let algo = SizedAlgoKind::from_unsized(algo_kind, algo_byte_len)?; compute_and_check_digest_from_file(filename_to_check, &expected_checksum, algo, opts) } @@ -1237,7 +1129,7 @@ fn process_non_algo_based_line( _ => (cli_algo_kind, cli_algo_length), }; - let algo = detect_algo(algo_kind, algo_byte_len)?; + let algo = SizedAlgoKind::from_unsized(algo_kind, algo_byte_len)?; compute_and_check_digest_from_file(filename_to_check, &expected_checksum, algo, opts) } @@ -1475,29 +1367,29 @@ pub fn digest_reader( } /// Calculates the length of the digest. -pub fn calculate_blake2b_length(length: usize) -> UResult> { - calculate_blake2b_length_str(length.to_string().as_str()) +pub fn calculate_blake2b_length(bit_length: usize) -> UResult> { + calculate_blake2b_length_str(bit_length.to_string().as_str()) } /// Calculates the length of the digest. -pub fn calculate_blake2b_length_str(length: &str) -> UResult> { +pub fn calculate_blake2b_length_str(bit_length: &str) -> UResult> { // Blake2b's length is parsed in an u64. - match length.parse::() { + match bit_length.parse::() { Ok(0) => Ok(None), // Error cases Ok(n) if n > 512 => { - show_error!("{}", ChecksumError::InvalidLength(length.into())); + show_error!("{}", ChecksumError::InvalidLength(bit_length.into())); Err(ChecksumError::LengthTooBigForBlake("BLAKE2b".into()).into()) } Err(e) if *e.kind() == IntErrorKind::PosOverflow => { - show_error!("{}", ChecksumError::InvalidLength(length.into())); + show_error!("{}", ChecksumError::InvalidLength(bit_length.into())); Err(ChecksumError::LengthTooBigForBlake("BLAKE2b".into()).into()) } - Err(_) => Err(ChecksumError::InvalidLength(length.into()).into()), + Err(_) => Err(ChecksumError::InvalidLength(bit_length.into()).into()), Ok(n) if n % 8 != 0 => { - show_error!("{}", ChecksumError::InvalidLength(length.into())); + show_error!("{}", ChecksumError::InvalidLength(bit_length.into())); Err(ChecksumError::LengthNotMultipleOf8.into()) } @@ -1636,98 +1528,6 @@ mod tests { assert_eq!(calculate_blake2b_length(256).unwrap(), Some(32)); } - // #[test] - // fn test_detect_algo() { - // assert_eq!( - // detect_algo(ALGORITHM_OPTIONS_SYSV, None).unwrap().name, - // ALGORITHM_OPTIONS_SYSV - // ); - // assert_eq!( - // detect_algo(ALGORITHM_OPTIONS_BSD, None).unwrap().name, - // ALGORITHM_OPTIONS_BSD - // ); - // assert_eq!( - // detect_algo(ALGORITHM_OPTIONS_CRC, None).unwrap().name, - // ALGORITHM_OPTIONS_CRC - // ); - // assert_eq!( - // detect_algo(ALGORITHM_OPTIONS_MD5, None).unwrap().name, - // ALGORITHM_OPTIONS_MD5 - // ); - // assert_eq!( - // detect_algo(ALGORITHM_OPTIONS_SHA1, None).unwrap().name, - // ALGORITHM_OPTIONS_SHA1 - // ); - // assert_eq!( - // detect_algo(ALGORITHM_OPTIONS_SHA224, None).unwrap().name, - // ALGORITHM_OPTIONS_SHA224.to_ascii_uppercase() - // ); - // assert_eq!( - // detect_algo(ALGORITHM_OPTIONS_SHA256, None).unwrap().name, - // ALGORITHM_OPTIONS_SHA256.to_ascii_uppercase() - // ); - // assert_eq!( - // detect_algo(ALGORITHM_OPTIONS_SHA384, None).unwrap().name, - // ALGORITHM_OPTIONS_SHA384.to_ascii_uppercase() - // ); - // assert_eq!( - // detect_algo(ALGORITHM_OPTIONS_SHA512, None).unwrap().name, - // ALGORITHM_OPTIONS_SHA512.to_ascii_uppercase() - // ); - // assert_eq!( - // detect_algo(ALGORITHM_OPTIONS_BLAKE2B, None).unwrap().name, - // ALGORITHM_OPTIONS_BLAKE2B - // ); - // assert_eq!( - // detect_algo(ALGORITHM_OPTIONS_BLAKE3, None).unwrap().name, - // ALGORITHM_OPTIONS_BLAKE3 - // ); - // assert_eq!( - // detect_algo(ALGORITHM_OPTIONS_SM3, None).unwrap().name, - // ALGORITHM_OPTIONS_SM3 - // ); - // assert_eq!( - // detect_algo(ALGORITHM_OPTIONS_SHAKE128, Some(128)) - // .unwrap() - // .name, - // ALGORITHM_OPTIONS_SHAKE128 - // ); - // assert_eq!( - // detect_algo(ALGORITHM_OPTIONS_SHAKE256, Some(256)) - // .unwrap() - // .name, - // ALGORITHM_OPTIONS_SHAKE256 - // ); - - // // Older versions of checksum used to detect the "sha3" prefix, but not - // // anymore. - // assert!(detect_algo("sha3_224", Some(224)).is_err()); - // assert!(detect_algo("sha3_256", Some(256)).is_err()); - // assert!(detect_algo("sha3_384", Some(384)).is_err()); - // assert!(detect_algo("sha3_512", Some(512)).is_err()); - - // let sha3_224 = detect_algo("sha3", Some(224)).unwrap(); - // assert_eq!(sha3_224.name, "SHA3-224"); - // assert_eq!(sha3_224.bits, 224); - // let sha3_256 = detect_algo("sha3", Some(256)).unwrap(); - // assert_eq!(sha3_256.name, "SHA3-256"); - // assert_eq!(sha3_256.bits, 256); - // let sha3_384 = detect_algo("sha3", Some(384)).unwrap(); - // assert_eq!(sha3_384.name, "SHA3-384"); - // assert_eq!(sha3_384.bits, 384); - // let sha3_512 = detect_algo("sha3", Some(512)).unwrap(); - // assert_eq!(sha3_512.name, "SHA3-512"); - // assert_eq!(sha3_512.bits, 512); - - // assert!(detect_algo("sha3", None).is_err()); - - // assert_eq!(detect_algo("sha2", Some(224)).unwrap().name, "SHA224"); - // assert_eq!(detect_algo("sha2", Some(256)).unwrap().name, "SHA256"); - // assert_eq!(detect_algo("sha2", Some(384)).unwrap().name, "SHA384"); - // assert_eq!(detect_algo("sha2", Some(512)).unwrap().name, "SHA512"); - - // assert!(detect_algo("sha2", None).is_err()); - // } #[test] fn test_algo_based_parser() { #[allow(clippy::type_complexity)] From 0b7289310d274bfe678a66a2e0cb253a82b9b6d6 Mon Sep 17 00:00:00 2001 From: Dorian Peron Date: Thu, 6 Nov 2025 16:04:54 +0100 Subject: [PATCH 075/182] checksum: Create a sub-module for checksum validation --- src/uu/cksum/src/cksum.rs | 9 +- src/uu/hashsum/src/hashsum.rs | 27 +- src/uucore/src/lib/features/checksum/mod.rs | 614 +++++++++++++++ .../{checksum.rs => checksum/validate.rs} | 712 ++---------------- 4 files changed, 690 insertions(+), 672 deletions(-) create mode 100644 src/uucore/src/lib/features/checksum/mod.rs rename src/uucore/src/lib/features/{checksum.rs => checksum/validate.rs} (66%) diff --git a/src/uu/cksum/src/cksum.rs b/src/uu/cksum/src/cksum.rs index b80139f67..a0a5c00df 100644 --- a/src/uu/cksum/src/cksum.rs +++ b/src/uu/cksum/src/cksum.rs @@ -12,10 +12,10 @@ use std::fs::File; use std::io::{BufReader, Read, Write, stdin, stdout}; use std::iter; use std::path::Path; +use uucore::checksum::validate::{ChecksumOptions, ChecksumVerbose, perform_checksum_validation}; use uucore::checksum::{ - AlgoKind, ChecksumError, ChecksumOptions, ChecksumVerbose, SUPPORTED_ALGORITHMS, SizedAlgoKind, - calculate_blake2b_length_str, digest_reader, perform_checksum_validation, - sanitize_sha2_sha3_length_str, + AlgoKind, ChecksumError, SUPPORTED_ALGORITHMS, SizedAlgoKind, calculate_blake2b_length_str, + digest_reader, sanitize_sha2_sha3_length_str, }; use uucore::translate; @@ -428,8 +428,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // Execute the checksum validation based on the presence of files or the use of stdin let verbose = ChecksumVerbose::new(status, quiet, warn); - let opts = ChecksumOptions { - binary: binary_flag, + let opts = ChecksumValidateOptions { ignore_missing, strict, verbose, diff --git a/src/uu/hashsum/src/hashsum.rs b/src/uu/hashsum/src/hashsum.rs index c9f600c32..61bd0f0ff 100644 --- a/src/uu/hashsum/src/hashsum.rs +++ b/src/uu/hashsum/src/hashsum.rs @@ -15,17 +15,17 @@ use std::io::{BufReader, Read, stdin}; use std::iter; use std::num::ParseIntError; use std::path::Path; -use uucore::checksum::ChecksumVerbose; -use uucore::checksum::calculate_blake2b_length; -use uucore::checksum::digest_reader; -use uucore::checksum::escape_filename; -use uucore::checksum::perform_checksum_validation; -use uucore::checksum::{AlgoKind, ChecksumError}; -use uucore::checksum::{ChecksumOptions, SizedAlgoKind}; + +use uucore::checksum::validate::{ + ChecksumValidateOptions, ChecksumVerbose, perform_checksum_validation, +}; +use uucore::checksum::{ + AlgoKind, ChecksumError, SizedAlgoKind, calculate_blake2b_length, digest_reader, + escape_filename, +}; use uucore::error::{UResult, strip_errno}; -use uucore::format_usage; use uucore::sum::Digest; -use uucore::translate; +use uucore::{format_usage, translate}; const NAME: &str = "hashsum"; // Using the same read buffer size as GNU @@ -200,16 +200,14 @@ pub fn uumain(mut args: impl uucore::Args) -> UResult<()> { // on Windows, allow --binary/--text to be used with --check // and keep the behavior of defaulting to binary #[cfg(not(windows))] - let binary = { + { let text_flag = matches.get_flag("text"); let binary_flag = matches.get_flag("binary"); if binary_flag || text_flag { return Err(ChecksumError::BinaryTextConflict.into()); } - - false - }; + } // Execute the checksum validation based on the presence of files or the use of stdin // Determine the source of input: a list of files or stdin. @@ -220,8 +218,7 @@ pub fn uumain(mut args: impl uucore::Args) -> UResult<()> { let verbose = ChecksumVerbose::new(status, quiet, warn); - let opts = ChecksumOptions { - binary, + let opts = ChecksumValidateOptions { ignore_missing, strict, verbose, diff --git a/src/uucore/src/lib/features/checksum/mod.rs b/src/uucore/src/lib/features/checksum/mod.rs new file mode 100644 index 000000000..29af7a491 --- /dev/null +++ b/src/uucore/src/lib/features/checksum/mod.rs @@ -0,0 +1,614 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. +// spell-checker:ignore anotherfile invalidchecksum JWZG FFFD xffname prefixfilename bytelen bitlen hexdigit rsplit + +use os_display::Quotable; +use std::{ + io::{self, Read}, + num::IntErrorKind, + path::Path, +}; + +use crate::{ + error::{UError, UResult}, + show_error, + sum::{ + Blake2b, Blake3, Bsd, CRC32B, Crc, Digest, DigestWriter, Md5, Sha1, Sha3_224, Sha3_256, + Sha3_384, Sha3_512, Sha224, Sha256, Sha384, Sha512, Shake128, Shake256, Sm3, SysV, + }, +}; +use thiserror::Error; + +pub mod validate; + +pub const ALGORITHM_OPTIONS_SYSV: &str = "sysv"; +pub const ALGORITHM_OPTIONS_BSD: &str = "bsd"; +pub const ALGORITHM_OPTIONS_CRC: &str = "crc"; +pub const ALGORITHM_OPTIONS_CRC32B: &str = "crc32b"; +pub const ALGORITHM_OPTIONS_MD5: &str = "md5"; +pub const ALGORITHM_OPTIONS_SHA1: &str = "sha1"; +pub const ALGORITHM_OPTIONS_SHA2: &str = "sha2"; +pub const ALGORITHM_OPTIONS_SHA3: &str = "sha3"; + +pub const ALGORITHM_OPTIONS_SHA224: &str = "sha224"; +pub const ALGORITHM_OPTIONS_SHA256: &str = "sha256"; +pub const ALGORITHM_OPTIONS_SHA384: &str = "sha384"; +pub const ALGORITHM_OPTIONS_SHA512: &str = "sha512"; +pub const ALGORITHM_OPTIONS_BLAKE2B: &str = "blake2b"; +pub const ALGORITHM_OPTIONS_BLAKE3: &str = "blake3"; +pub const ALGORITHM_OPTIONS_SM3: &str = "sm3"; +pub const ALGORITHM_OPTIONS_SHAKE128: &str = "shake128"; +pub const ALGORITHM_OPTIONS_SHAKE256: &str = "shake256"; + +pub const SUPPORTED_ALGORITHMS: [&str; 17] = [ + ALGORITHM_OPTIONS_SYSV, + ALGORITHM_OPTIONS_BSD, + ALGORITHM_OPTIONS_CRC, + ALGORITHM_OPTIONS_CRC32B, + ALGORITHM_OPTIONS_MD5, + ALGORITHM_OPTIONS_SHA1, + ALGORITHM_OPTIONS_SHA2, + ALGORITHM_OPTIONS_SHA3, + ALGORITHM_OPTIONS_BLAKE2B, + ALGORITHM_OPTIONS_SM3, + // Legacy aliases for -a sha2 -l xxx + ALGORITHM_OPTIONS_SHA224, + ALGORITHM_OPTIONS_SHA256, + ALGORITHM_OPTIONS_SHA384, + ALGORITHM_OPTIONS_SHA512, + // Extra algorithms that are not valid `cksum --algorithm` as per GNU. + // TODO: Should we keep them or drop them to align our support with GNU ? + ALGORITHM_OPTIONS_BLAKE3, + ALGORITHM_OPTIONS_SHAKE128, + ALGORITHM_OPTIONS_SHAKE256, +]; + +/// Represents an algorithm kind. In some cases, it is not sufficient by itself +/// to know which algorithm to use exactly, because it lacks a digest length, +/// which is why [`SizedAlgoKind`] exists. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AlgoKind { + Sysv, + Bsd, + Crc, + Crc32b, + Md5, + Sm3, + Sha1, + Sha2, + Sha3, + Blake2b, + + // Available in cksum for backward compatibility + Sha224, + Sha256, + Sha384, + Sha512, + + // Not available in cksum + Shake128, + Shake256, + Blake3, +} + +impl AlgoKind { + /// Parses an [`AlgoKind`] from a string, only accepting valid cksum + /// `--algorithm` values. + pub fn from_cksum(algo: impl AsRef) -> UResult { + use AlgoKind::*; + Ok(match algo.as_ref() { + ALGORITHM_OPTIONS_SYSV => Sysv, + ALGORITHM_OPTIONS_BSD => Bsd, + ALGORITHM_OPTIONS_CRC => Crc, + ALGORITHM_OPTIONS_CRC32B => Crc32b, + ALGORITHM_OPTIONS_MD5 => Md5, + ALGORITHM_OPTIONS_SHA1 => Sha1, + ALGORITHM_OPTIONS_SHA2 => Sha2, + ALGORITHM_OPTIONS_SHA3 => Sha3, + ALGORITHM_OPTIONS_BLAKE2B => Blake2b, + ALGORITHM_OPTIONS_SM3 => Sm3, + + // For backward compatibility + ALGORITHM_OPTIONS_SHA224 => Sha224, + ALGORITHM_OPTIONS_SHA256 => Sha256, + ALGORITHM_OPTIONS_SHA384 => Sha384, + ALGORITHM_OPTIONS_SHA512 => Sha512, + _ => return Err(ChecksumError::UnknownAlgorithm(algo.as_ref().to_string()).into()), + }) + } + + /// Parses an algo kind from a string, accepting standalone binary names. + pub fn from_bin_name(algo: impl AsRef) -> UResult { + use AlgoKind::*; + Ok(match algo.as_ref() { + "md5sum" => Md5, + "sha1sum" => Sha1, + "sha224sum" => Sha224, + "sha256sum" => Sha256, + "sha384sum" => Sha384, + "sha512sum" => Sha512, + "sha3sum" => Sha3, + "b2sum" => Blake2b, + + _ => return Err(ChecksumError::UnknownAlgorithm(algo.as_ref().to_string()).into()), + }) + } + + /// Returns a string corresponding to the algorithm kind. + pub fn to_uppercase(self) -> &'static str { + use AlgoKind::*; + match self { + // Legacy algorithms + Sysv => "SYSV", + Bsd => "BSD", + Crc => "CRC", + Crc32b => "CRC32B", + + Md5 => "MD5", + Sm3 => "SM3", + Sha1 => "SHA1", + Sha2 => "SHA2", + Sha3 => "SHA3", + Blake2b => "BLAKE2b", // Note the lowercase b in the end here. + + // For backward compatibility + Sha224 => "SHA224", + Sha256 => "SHA256", + Sha384 => "SHA384", + Sha512 => "SHA512", + + Shake128 => "SHAKE128", + Shake256 => "SHAKE256", + Blake3 => "BLAKE3", + } + } + + /// Returns a string corresponding to the algorithm option in cksum `-a` + pub fn to_lowercase(self) -> &'static str { + use AlgoKind::*; + match self { + Sysv => "sysv", + Bsd => "bsd", + Crc => "crc", + Crc32b => "crc32b", + Md5 => "md5", + Sm3 => "sm3", + Sha1 => "sha1", + Sha2 => "sha2", + Sha3 => "sha3", + Blake2b => "blake2b", + + // For backward compatibility + Sha224 => "sha224", + Sha256 => "sha256", + Sha384 => "sha384", + Sha512 => "sha512", + + Shake128 => "shake128", + Shake256 => "shake256", + Blake3 => "blake3", + } + } + + pub fn is_legacy(self) -> bool { + use AlgoKind::*; + matches!(self, Sysv | Bsd | Crc | Crc32b) + } +} + +/// Holds a length for a SHA2 of SHA3 algorithm kind. +#[derive(Debug, Clone, Copy)] +pub enum ShaLength { + Len224, + Len256, + Len384, + Len512, +} + +impl ShaLength { + pub fn as_usize(self) -> usize { + match self { + Self::Len224 => 224, + Self::Len256 => 256, + Self::Len384 => 384, + Self::Len512 => 512, + } + } +} + +impl TryFrom for ShaLength { + type Error = ChecksumError; + + fn try_from(value: usize) -> Result { + use ShaLength::*; + match value { + 224 => Ok(Len224), + 256 => Ok(Len256), + 384 => Ok(Len384), + 512 => Ok(Len512), + _ => Err(ChecksumError::InvalidLengthForSha(value.to_string())), + } + } +} + +/// Represents an actual determined algorithm. +#[derive(Debug, Clone, Copy)] +pub enum SizedAlgoKind { + Sysv, + Bsd, + Crc, + Crc32b, + Md5, + Sm3, + Sha1, + Blake3, + Sha2(ShaLength), + Sha3(ShaLength), + // Note: we store Blake2b's length as BYTES. + Blake2b(Option), + Shake128(usize), + Shake256(usize), +} + +impl SizedAlgoKind { + pub fn from_unsized(kind: AlgoKind, byte_length: Option) -> UResult { + use AlgoKind as ak; + match (kind, byte_length) { + ( + ak::Sysv + | ak::Bsd + | ak::Crc + | ak::Crc32b + | ak::Md5 + | ak::Sm3 + | ak::Sha1 + | ak::Blake3 + | ak::Sha224 + | ak::Sha256 + | ak::Sha384 + | ak::Sha512, + Some(_), + ) => Err(ChecksumError::LengthOnlyForBlake2bSha2Sha3.into()), + + (ak::Sysv, _) => Ok(Self::Sysv), + (ak::Bsd, _) => Ok(Self::Bsd), + (ak::Crc, _) => Ok(Self::Crc), + (ak::Crc32b, _) => Ok(Self::Crc32b), + (ak::Md5, _) => Ok(Self::Md5), + (ak::Sm3, _) => Ok(Self::Sm3), + (ak::Sha1, _) => Ok(Self::Sha1), + (ak::Blake3, _) => Ok(Self::Blake3), + + (ak::Shake128, Some(l)) => Ok(Self::Shake128(l)), + (ak::Shake256, Some(l)) => Ok(Self::Shake256(l)), + (ak::Sha2, Some(l)) => Ok(Self::Sha2(ShaLength::try_from(l)?)), + (ak::Sha3, Some(l)) => Ok(Self::Sha3(ShaLength::try_from(l)?)), + (algo @ (ak::Sha2 | ak::Sha3), None) => { + Err(ChecksumError::LengthRequiredForSha(algo.to_lowercase().into()).into()) + } + // [`calculate_blake2b_length`] expects a length in bits but we + // have a length in bytes. + (ak::Blake2b, Some(l)) => Ok(Self::Blake2b(calculate_blake2b_length(8 * l)?)), + (ak::Blake2b, None) => Ok(Self::Blake2b(None)), + + (ak::Sha224, None) => Ok(Self::Sha2(ShaLength::Len224)), + (ak::Sha256, None) => Ok(Self::Sha2(ShaLength::Len256)), + (ak::Sha384, None) => Ok(Self::Sha2(ShaLength::Len384)), + (ak::Sha512, None) => Ok(Self::Sha2(ShaLength::Len512)), + (_, None) => Err(ChecksumError::LengthRequired(kind.to_uppercase().into()).into()), + } + } + + pub fn to_tag(self) -> String { + use SizedAlgoKind::*; + match self { + Md5 => "MD5".into(), + Sm3 => "SM3".into(), + Sha1 => "SHA1".into(), + Blake3 => "BLAKE3".into(), + Sha2(len) => format!("SHA{}", len.as_usize()), + Sha3(len) => format!("SHA3-{}", len.as_usize()), + Blake2b(Some(byte_len)) => format!("BLAKE2b-{}", byte_len * 8), + Blake2b(None) => "BLAKE2b".into(), + Shake128(_) => "SHAKE128".into(), + Shake256(_) => "SHAKE256".into(), + Sysv | Bsd | Crc | Crc32b => panic!("Should not be used for tagging"), + } + } + + pub fn create_digest(&self) -> Box { + use ShaLength::*; + match self { + Self::Sysv => Box::new(SysV::new()), + Self::Bsd => Box::new(Bsd::new()), + Self::Crc => Box::new(Crc::new()), + Self::Crc32b => Box::new(CRC32B::new()), + Self::Md5 => Box::new(Md5::new()), + Self::Sm3 => Box::new(Sm3::new()), + Self::Sha1 => Box::new(Sha1::new()), + Self::Blake3 => Box::new(Blake3::new()), + Self::Sha2(Len224) => Box::new(Sha224::new()), + Self::Sha2(Len256) => Box::new(Sha256::new()), + Self::Sha2(Len384) => Box::new(Sha384::new()), + Self::Sha2(Len512) => Box::new(Sha512::new()), + Self::Sha3(Len224) => Box::new(Sha3_224::new()), + Self::Sha3(Len256) => Box::new(Sha3_256::new()), + Self::Sha3(Len384) => Box::new(Sha3_384::new()), + Self::Sha3(Len512) => Box::new(Sha3_512::new()), + Self::Blake2b(Some(byte_len)) => Box::new(Blake2b::with_output_bytes(*byte_len)), + Self::Blake2b(None) => Box::new(Blake2b::new()), + Self::Shake128(_) => Box::new(Shake128::new()), + Self::Shake256(_) => Box::new(Shake256::new()), + } + } + + pub fn bitlen(&self) -> usize { + use SizedAlgoKind::*; + match self { + Sysv => 512, + Bsd => 1024, + Crc => 256, + Crc32b => 32, + Md5 => 128, + Sm3 => 512, + Sha1 => 160, + Blake3 => 256, + Sha2(len) => len.as_usize(), + Sha3(len) => len.as_usize(), + Blake2b(len) => len.unwrap_or(512), + Shake128(len) => *len, + Shake256(len) => *len, + } + } + pub fn is_legacy(&self) -> bool { + use SizedAlgoKind::*; + matches!(self, Sysv | Bsd | Crc | Crc32b) + } +} + +#[derive(Debug, Error)] +pub enum ChecksumError { + #[error("the --raw option is not supported with multiple files")] + RawMultipleFiles, + #[error("the --ignore-missing option is meaningful only when verifying checksums")] + IgnoreNotCheck, + #[error("the --strict option is meaningful only when verifying checksums")] + StrictNotCheck, + #[error("the --quiet option is meaningful only when verifying checksums")] + QuietNotCheck, + + // --length sanitization errors + #[error("--length required for {}", .0.quote())] + LengthRequired(String), + #[error("invalid length: {}", .0.quote())] + InvalidLength(String), + #[error("maximum digest length for {} is 512 bits", .0.quote())] + LengthTooBigForBlake(String), + #[error("length is not a multiple of 8")] + LengthNotMultipleOf8, + #[error("digest length for {} must be 224, 256, 384, or 512", .0.quote())] + InvalidLengthForSha(String), + #[error("--algorithm={0} requires specifying --length 224, 256, 384, or 512")] + LengthRequiredForSha(String), + #[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}}")] + AlgorithmNotSupportedWithCheck, + #[error("You cannot combine multiple hash algorithms!")] + CombineMultipleAlgorithms, + #[error("Needs an algorithm to hash with.\nUse --help for more information.")] + NeedAlgorithmToHash, + #[error("unknown algorithm: {0}: clap should have prevented this case")] + UnknownAlgorithm(String), + #[error("")] + Io(#[from] io::Error), +} + +impl UError for ChecksumError { + fn code(&self) -> i32 { + 1 + } +} + +pub fn digest_reader( + digest: &mut Box, + reader: &mut T, + binary: bool, + output_bits: usize, +) -> io::Result<(String, usize)> { + digest.reset(); + + // Read bytes from `reader` and write those bytes to `digest`. + // + // If `binary` is `false` and the operating system is Windows, then + // `DigestWriter` replaces "\r\n" with "\n" before it writes the + // bytes into `digest`. Otherwise, it just inserts the bytes as-is. + // + // In order to support replacing "\r\n", we must call `finalize()` + // in order to support the possibility that the last character read + // from the reader was "\r". (This character gets buffered by + // `DigestWriter` and only written if the following character is + // "\n". But when "\r" is the last character read, we need to force + // it to be written.) + let mut digest_writer = DigestWriter::new(digest, binary); + let output_size = std::io::copy(reader, &mut digest_writer)? as usize; + digest_writer.finalize(); + + if digest.output_bits() > 0 { + Ok((digest.result_str(), output_size)) + } else { + // Assume it's SHAKE. result_str() doesn't work with shake (as of 8/30/2016) + let mut bytes = vec![0; output_bits.div_ceil(8)]; + digest.hash_finalize(&mut bytes); + Ok((hex::encode(bytes), output_size)) + } +} + +/// Calculates the length of the digest. +pub fn calculate_blake2b_length(bit_length: usize) -> UResult> { + calculate_blake2b_length_str(bit_length.to_string().as_str()) +} + +/// Calculates the length of the digest. +pub fn calculate_blake2b_length_str(bit_length: &str) -> UResult> { + // Blake2b's length is parsed in an u64. + match bit_length.parse::() { + Ok(0) => Ok(None), + + // Error cases + Ok(n) if n > 512 => { + show_error!("{}", ChecksumError::InvalidLength(bit_length.into())); + Err(ChecksumError::LengthTooBigForBlake("BLAKE2b".into()).into()) + } + Err(e) if *e.kind() == IntErrorKind::PosOverflow => { + show_error!("{}", ChecksumError::InvalidLength(bit_length.into())); + Err(ChecksumError::LengthTooBigForBlake("BLAKE2b".into()).into()) + } + Err(_) => Err(ChecksumError::InvalidLength(bit_length.into()).into()), + + Ok(n) if n % 8 != 0 => { + show_error!("{}", ChecksumError::InvalidLength(bit_length.into())); + Err(ChecksumError::LengthNotMultipleOf8.into()) + } + + // Valid cases + + // When length is 512, it is blake2b's default. So, don't show it + Ok(512) => Ok(None), + // Divide by 8, as our blake2b implementation expects bytes instead of bits. + Ok(n) => Ok(Some(n / 8)), + } +} + +pub fn validate_sha2_sha3_length(algo_name: AlgoKind, length: Option) -> UResult { + match length { + Some(224) => Ok(ShaLength::Len224), + Some(256) => Ok(ShaLength::Len256), + Some(384) => Ok(ShaLength::Len384), + Some(512) => Ok(ShaLength::Len512), + Some(len) => { + show_error!("{}", ChecksumError::InvalidLength(len.to_string())); + Err(ChecksumError::InvalidLengthForSha(algo_name.to_uppercase().into()).into()) + } + None => Err(ChecksumError::LengthRequiredForSha(algo_name.to_lowercase().into()).into()), + } +} + +pub fn sanitize_sha2_sha3_length_str(algo_kind: AlgoKind, length: &str) -> UResult { + // There is a difference in the errors sent when the length is not a number + // vs. its an invalid number. + // + // When inputting an invalid number, an extra error message it printed to + // remind of the accepted inputs. + let len = match length.parse::() { + Ok(l) => l, + // Note: Positive overflow while parsing counts as an invalid number, + // but a number still. + Err(e) if *e.kind() == IntErrorKind::PosOverflow => { + show_error!("{}", ChecksumError::InvalidLength(length.into())); + return Err(ChecksumError::InvalidLengthForSha(algo_kind.to_uppercase().into()).into()); + } + Err(_) => return Err(ChecksumError::InvalidLength(length.into()).into()), + }; + + if [224, 256, 384, 512].contains(&len) { + Ok(len) + } else { + show_error!("{}", ChecksumError::InvalidLength(length.into())); + Err(ChecksumError::InvalidLengthForSha(algo_kind.to_uppercase().into()).into()) + } +} + +pub fn unescape_filename(filename: &[u8]) -> (Vec, &'static str) { + let mut unescaped = Vec::with_capacity(filename.len()); + let mut byte_iter = filename.iter().peekable(); + loop { + let Some(byte) = byte_iter.next() else { + break; + }; + if *byte == b'\\' { + match byte_iter.next() { + Some(b'\\') => unescaped.push(b'\\'), + Some(b'n') => unescaped.push(b'\n'), + Some(b'r') => unescaped.push(b'\r'), + Some(x) => { + unescaped.push(b'\\'); + unescaped.push(*x); + } + _ => {} + } + } else { + unescaped.push(*byte); + } + } + let prefix = if unescaped == filename { "" } else { "\\" }; + (unescaped, prefix) +} + +pub fn escape_filename(filename: &Path) -> (String, &'static str) { + let original = filename.as_os_str().to_string_lossy(); + let escaped = original + .replace('\\', "\\\\") + .replace('\n', "\\n") + .replace('\r', "\\r"); + let prefix = if escaped == original { "" } else { "\\" }; + (escaped, prefix) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_unescape_filename() { + let (unescaped, prefix) = unescape_filename(b"test\\nfile.txt"); + assert_eq!(unescaped, b"test\nfile.txt"); + assert_eq!(prefix, "\\"); + let (unescaped, prefix) = unescape_filename(b"test\\nfile.txt"); + assert_eq!(unescaped, b"test\nfile.txt"); + assert_eq!(prefix, "\\"); + + let (unescaped, prefix) = unescape_filename(b"test\\rfile.txt"); + assert_eq!(unescaped, b"test\rfile.txt"); + assert_eq!(prefix, "\\"); + + let (unescaped, prefix) = unescape_filename(b"test\\\\file.txt"); + assert_eq!(unescaped, b"test\\file.txt"); + assert_eq!(prefix, "\\"); + } + + #[test] + fn test_escape_filename() { + let (escaped, prefix) = escape_filename(Path::new("testfile.txt")); + assert_eq!(escaped, "testfile.txt"); + assert_eq!(prefix, ""); + + let (escaped, prefix) = escape_filename(Path::new("test\nfile.txt")); + assert_eq!(escaped, "test\\nfile.txt"); + assert_eq!(prefix, "\\"); + + let (escaped, prefix) = escape_filename(Path::new("test\rfile.txt")); + assert_eq!(escaped, "test\\rfile.txt"); + assert_eq!(prefix, "\\"); + + let (escaped, prefix) = escape_filename(Path::new("test\\file.txt")); + assert_eq!(escaped, "test\\\\file.txt"); + assert_eq!(prefix, "\\"); + } + + #[test] + fn test_calculate_blake2b_length() { + assert_eq!(calculate_blake2b_length(0).unwrap(), None); + assert!(calculate_blake2b_length(10).is_err()); + assert!(calculate_blake2b_length(520).is_err()); + assert_eq!(calculate_blake2b_length(512).unwrap(), None); + assert_eq!(calculate_blake2b_length(256).unwrap(), Some(32)); + } +} diff --git a/src/uucore/src/lib/features/checksum.rs b/src/uucore/src/lib/features/checksum/validate.rs similarity index 66% rename from src/uucore/src/lib/features/checksum.rs rename to src/uucore/src/lib/features/checksum/validate.rs index aed5b9111..a1d851a05 100644 --- a/src/uucore/src/lib/features/checksum.rs +++ b/src/uucore/src/lib/features/checksum/validate.rs @@ -2,377 +2,72 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore anotherfile invalidchecksum JWZG FFFD xffname prefixfilename bytelen bitlen hexdigit rsplit + +// spell-checker:ignore rsplit hexdigit bitlen bytelen invalidchecksum xffname + +use std::borrow::Cow; +use std::ffi::OsStr; +use std::fmt::Display; +use std::fs::File; +use std::io::{self, BufReader, Read, Write, stdin}; use data_encoding::BASE64; use os_display::Quotable; -use std::{ - borrow::Cow, - ffi::OsStr, - fmt::Display, - fs::File, - io::{self, BufReader, Read, Write, stdin}, - num::IntErrorKind, - path::Path, - str, -}; +use crate::checksum::{AlgoKind, ChecksumError, SizedAlgoKind, digest_reader, unescape_filename}; +use crate::error::{FromIo, UError, UResult, USimpleError}; +use crate::quoting_style::{QuotingStyle, locale_aware_escape_name}; use crate::{ - error::{FromIo, UError, UResult, USimpleError}, - os_str_as_bytes, os_str_from_bytes, - quoting_style::{QuotingStyle, locale_aware_escape_name}, - read_os_string_lines, show, show_error, show_warning_caps, - sum::{ - Blake2b, Blake3, Bsd, CRC32B, Crc, Digest, DigestWriter, Md5, Sha1, Sha3_224, Sha3_256, - Sha3_384, Sha3_512, Sha224, Sha256, Sha384, Sha512, Shake128, Shake256, Sm3, SysV, - }, + os_str_as_bytes, os_str_from_bytes, read_os_string_lines, show, show_error, show_warning_caps, util_name, }; -use thiserror::Error; -pub const ALGORITHM_OPTIONS_SYSV: &str = "sysv"; -pub const ALGORITHM_OPTIONS_BSD: &str = "bsd"; -pub const ALGORITHM_OPTIONS_CRC: &str = "crc"; -pub const ALGORITHM_OPTIONS_CRC32B: &str = "crc32b"; -pub const ALGORITHM_OPTIONS_MD5: &str = "md5"; -pub const ALGORITHM_OPTIONS_SHA1: &str = "sha1"; -pub const ALGORITHM_OPTIONS_SHA2: &str = "sha2"; -pub const ALGORITHM_OPTIONS_SHA3: &str = "sha3"; - -pub const ALGORITHM_OPTIONS_SHA224: &str = "sha224"; -pub const ALGORITHM_OPTIONS_SHA256: &str = "sha256"; -pub const ALGORITHM_OPTIONS_SHA384: &str = "sha384"; -pub const ALGORITHM_OPTIONS_SHA512: &str = "sha512"; -pub const ALGORITHM_OPTIONS_BLAKE2B: &str = "blake2b"; -pub const ALGORITHM_OPTIONS_BLAKE3: &str = "blake3"; -pub const ALGORITHM_OPTIONS_SM3: &str = "sm3"; -pub const ALGORITHM_OPTIONS_SHAKE128: &str = "shake128"; -pub const ALGORITHM_OPTIONS_SHAKE256: &str = "shake256"; - -pub const SUPPORTED_ALGORITHMS: [&str; 17] = [ - ALGORITHM_OPTIONS_SYSV, - ALGORITHM_OPTIONS_BSD, - ALGORITHM_OPTIONS_CRC, - ALGORITHM_OPTIONS_CRC32B, - ALGORITHM_OPTIONS_MD5, - ALGORITHM_OPTIONS_SHA1, - ALGORITHM_OPTIONS_SHA2, - ALGORITHM_OPTIONS_SHA3, - ALGORITHM_OPTIONS_BLAKE2B, - ALGORITHM_OPTIONS_SM3, - // Legacy aliases for -a sha2 -l xxx - ALGORITHM_OPTIONS_SHA224, - ALGORITHM_OPTIONS_SHA256, - ALGORITHM_OPTIONS_SHA384, - ALGORITHM_OPTIONS_SHA512, - // Extra algorithms that are not valid `cksum --algorithm` as per GNU. - // TODO: Should we keep them or drop them to align our support with GNU ? - ALGORITHM_OPTIONS_BLAKE3, - ALGORITHM_OPTIONS_SHAKE128, - ALGORITHM_OPTIONS_SHAKE256, -]; - -/// Represents an algorithm kind. In some cases, it is not sufficient by itself -/// to know which algorithm to use exactly, because it lacks a digest length, -/// which is why [`SizedAlgoKind`] exists. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum AlgoKind { - Sysv, - Bsd, - Crc, - Crc32b, - Md5, - Sm3, - Sha1, - Sha2, - Sha3, - Blake2b, - - // Available in cksum for backward compatibility - Sha224, - Sha256, - Sha384, - Sha512, - - // Not available in cksum - Shake128, - Shake256, - Blake3, +/// To what level should checksum validation print logging info. +#[derive(Debug, PartialEq, Eq, PartialOrd, Clone, Copy, Default)] +pub enum ChecksumVerbose { + Status, + Quiet, + #[default] + Normal, + Warning, } -impl AlgoKind { - /// Parses an [`AlgoKind`] from a string, only accepting valid cksum - /// `--algorithm` values. - pub fn from_cksum(algo: impl AsRef) -> UResult { - use AlgoKind::*; - Ok(match algo.as_ref() { - ALGORITHM_OPTIONS_SYSV => Sysv, - ALGORITHM_OPTIONS_BSD => Bsd, - ALGORITHM_OPTIONS_CRC => Crc, - ALGORITHM_OPTIONS_CRC32B => Crc32b, - ALGORITHM_OPTIONS_MD5 => Md5, - ALGORITHM_OPTIONS_SHA1 => Sha1, - ALGORITHM_OPTIONS_SHA2 => Sha2, - ALGORITHM_OPTIONS_SHA3 => Sha3, - ALGORITHM_OPTIONS_BLAKE2B => Blake2b, - ALGORITHM_OPTIONS_SM3 => Sm3, +impl ChecksumVerbose { + pub fn new(status: bool, quiet: bool, warn: bool) -> Self { + use ChecksumVerbose::*; - // For backward compatibility - ALGORITHM_OPTIONS_SHA224 => Sha224, - ALGORITHM_OPTIONS_SHA256 => Sha256, - ALGORITHM_OPTIONS_SHA384 => Sha384, - ALGORITHM_OPTIONS_SHA512 => Sha512, - _ => return Err(ChecksumError::UnknownAlgorithm(algo.as_ref().to_string()).into()), - }) - } - - /// Parses an algo kind from a string, accepting standalone binary names. - pub fn from_bin_name(algo: impl AsRef) -> UResult { - use AlgoKind::*; - Ok(match algo.as_ref() { - "md5sum" => Md5, - "sha1sum" => Sha1, - "sha224sum" => Sha224, - "sha256sum" => Sha256, - "sha384sum" => Sha384, - "sha512sum" => Sha512, - "sha3sum" => Sha3, - "b2sum" => Blake2b, - - _ => return Err(ChecksumError::UnknownAlgorithm(algo.as_ref().to_string()).into()), - }) - } - - /// Returns a string corresponding to the algorithm kind. - pub fn to_uppercase(self) -> &'static str { - use AlgoKind::*; - match self { - // Legacy algorithms - Sysv => "SYSV", - Bsd => "BSD", - Crc => "CRC", - Crc32b => "CRC32B", - - Md5 => "MD5", - Sm3 => "SM3", - Sha1 => "SHA1", - Sha2 => "SHA2", - Sha3 => "SHA3", - Blake2b => "BLAKE2b", // Note the lowercase b in the end here. - - // For backward compatibility - Sha224 => "SHA224", - Sha256 => "SHA256", - Sha384 => "SHA384", - Sha512 => "SHA512", - - Shake128 => "SHAKE128", - Shake256 => "SHAKE256", - Blake3 => "BLAKE3", + // Assume only one of the three booleans will be enabled at once. + // This is ensured by clap's overriding arguments. + match (status, quiet, warn) { + (true, _, _) => Status, + (_, true, _) => Quiet, + (_, _, true) => Warning, + _ => Normal, } } - /// Returns a string corresponding to the algorithm option in cksum `-a` - pub fn to_lowercase(self) -> &'static str { - use AlgoKind::*; - match self { - Sysv => "sysv", - Bsd => "bsd", - Crc => "crc", - Crc32b => "crc32b", - Md5 => "md5", - Sm3 => "sm3", - Sha1 => "sha1", - Sha2 => "sha2", - Sha3 => "sha3", - Blake2b => "blake2b", - - // For backward compatibility - Sha224 => "sha224", - Sha256 => "sha256", - Sha384 => "sha384", - Sha512 => "sha512", - - Shake128 => "shake128", - Shake256 => "shake256", - Blake3 => "blake3", - } + #[inline] + pub fn over_status(self) -> bool { + self > Self::Status } - pub fn is_legacy(self) -> bool { - use AlgoKind::*; - matches!(self, Sysv | Bsd | Crc | Crc32b) + #[inline] + pub fn over_quiet(self) -> bool { + self > Self::Quiet + } + + #[inline] + pub fn at_least_warning(self) -> bool { + self >= Self::Warning } } -/// Holds a length for a SHA2 of SHA3 algorithm kind. -#[derive(Debug, Clone, Copy)] -pub enum ShaLength { - Len224, - Len256, - Len384, - Len512, -} - -impl ShaLength { - pub fn as_usize(self) -> usize { - match self { - Self::Len224 => 224, - Self::Len256 => 256, - Self::Len384 => 384, - Self::Len512 => 512, - } - } -} - -impl TryFrom for ShaLength { - type Error = ChecksumError; - - fn try_from(value: usize) -> Result { - use ShaLength::*; - match value { - 224 => Ok(Len224), - 256 => Ok(Len256), - 384 => Ok(Len384), - 512 => Ok(Len512), - _ => Err(ChecksumError::InvalidLengthForSha(value.to_string())), - } - } -} - -/// Represents an actual determined algorithm. -#[derive(Debug, Clone, Copy)] -pub enum SizedAlgoKind { - Sysv, - Bsd, - Crc, - Crc32b, - Md5, - Sm3, - Sha1, - Blake3, - Sha2(ShaLength), - Sha3(ShaLength), - // Note: we store Blake2b's length as BYTES. - Blake2b(Option), - Shake128(usize), - Shake256(usize), -} - -impl SizedAlgoKind { - pub fn from_unsized(kind: AlgoKind, byte_length: Option) -> UResult { - use AlgoKind as ak; - match (kind, byte_length) { - ( - ak::Sysv - | ak::Bsd - | ak::Crc - | ak::Crc32b - | ak::Md5 - | ak::Sm3 - | ak::Sha1 - | ak::Blake3 - | ak::Sha224 - | ak::Sha256 - | ak::Sha384 - | ak::Sha512, - Some(_), - ) => Err(ChecksumError::LengthOnlyForBlake2bSha2Sha3.into()), - - (ak::Sysv, _) => Ok(Self::Sysv), - (ak::Bsd, _) => Ok(Self::Bsd), - (ak::Crc, _) => Ok(Self::Crc), - (ak::Crc32b, _) => Ok(Self::Crc32b), - (ak::Md5, _) => Ok(Self::Md5), - (ak::Sm3, _) => Ok(Self::Sm3), - (ak::Sha1, _) => Ok(Self::Sha1), - (ak::Blake3, _) => Ok(Self::Blake3), - - (ak::Shake128, Some(l)) => Ok(Self::Shake128(l)), - (ak::Shake256, Some(l)) => Ok(Self::Shake256(l)), - (ak::Sha2, Some(l)) => Ok(Self::Sha2(ShaLength::try_from(l)?)), - (ak::Sha3, Some(l)) => Ok(Self::Sha3(ShaLength::try_from(l)?)), - (algo @ (ak::Sha2 | ak::Sha3), None) => { - Err(ChecksumError::LengthRequiredForSha(algo.to_lowercase().into()).into()) - } - // [`calculate_blake2b_length`] expects a length in bits but we - // have a length in bytes. - (ak::Blake2b, Some(l)) => Ok(Self::Blake2b(calculate_blake2b_length(8 * l)?)), - (ak::Blake2b, None) => Ok(Self::Blake2b(None)), - - (ak::Sha224, None) => Ok(Self::Sha2(ShaLength::Len224)), - (ak::Sha256, None) => Ok(Self::Sha2(ShaLength::Len256)), - (ak::Sha384, None) => Ok(Self::Sha2(ShaLength::Len384)), - (ak::Sha512, None) => Ok(Self::Sha2(ShaLength::Len512)), - (_, None) => Err(ChecksumError::LengthRequired(kind.to_uppercase().into()).into()), - } - } - - pub fn to_tag(self) -> String { - use SizedAlgoKind::*; - match self { - Md5 => "MD5".into(), - Sm3 => "SM3".into(), - Sha1 => "SHA1".into(), - Blake3 => "BLAKE3".into(), - Sha2(len) => format!("SHA{}", len.as_usize()), - Sha3(len) => format!("SHA3-{}", len.as_usize()), - Blake2b(Some(byte_len)) => format!("BLAKE2b-{}", byte_len * 8), - Blake2b(None) => "BLAKE2b".into(), - Shake128(_) => "SHAKE128".into(), - Shake256(_) => "SHAKE256".into(), - Sysv | Bsd | Crc | Crc32b => panic!("Should not be used for tagging"), - } - } - - pub fn create_digest(&self) -> Box { - use ShaLength::*; - match self { - Self::Sysv => Box::new(SysV::new()), - Self::Bsd => Box::new(Bsd::new()), - Self::Crc => Box::new(Crc::new()), - Self::Crc32b => Box::new(CRC32B::new()), - Self::Md5 => Box::new(Md5::new()), - Self::Sm3 => Box::new(Sm3::new()), - Self::Sha1 => Box::new(Sha1::new()), - Self::Blake3 => Box::new(Blake3::new()), - Self::Sha2(Len224) => Box::new(Sha224::new()), - Self::Sha2(Len256) => Box::new(Sha256::new()), - Self::Sha2(Len384) => Box::new(Sha384::new()), - Self::Sha2(Len512) => Box::new(Sha512::new()), - Self::Sha3(Len224) => Box::new(Sha3_224::new()), - Self::Sha3(Len256) => Box::new(Sha3_256::new()), - Self::Sha3(Len384) => Box::new(Sha3_384::new()), - Self::Sha3(Len512) => Box::new(Sha3_512::new()), - Self::Blake2b(Some(byte_len)) => Box::new(Blake2b::with_output_bytes(*byte_len)), - Self::Blake2b(None) => Box::new(Blake2b::new()), - Self::Shake128(_) => Box::new(Shake128::new()), - Self::Shake256(_) => Box::new(Shake256::new()), - } - } - - pub fn bitlen(&self) -> usize { - use SizedAlgoKind::*; - match self { - Sysv => 512, - Bsd => 1024, - Crc => 256, - Crc32b => 32, - Md5 => 128, - Sm3 => 512, - Sha1 => 160, - Blake3 => 256, - Sha2(len) => len.as_usize(), - Sha3(len) => len.as_usize(), - Blake2b(len) => len.unwrap_or(512), - Shake128(len) => *len, - Shake256(len) => *len, - } - } - pub fn is_legacy(&self) -> bool { - use SizedAlgoKind::*; - matches!(self, Sysv | Bsd | Crc | Crc32b) - } +/// This struct regroups CLI flags. +#[derive(Debug, Default, Clone, Copy)] +pub struct ChecksumValidateOptions { + pub ignore_missing: bool, + pub strict: bool, + pub verbose: ChecksumVerbose, } /// This structure holds the count of checksum test lines' outcomes. @@ -453,103 +148,6 @@ impl From for FileCheckError { } } -#[derive(Debug, PartialEq, Eq, PartialOrd, Clone, Copy, Default)] -pub enum ChecksumVerbose { - Status, - Quiet, - #[default] - Normal, - Warning, -} - -impl ChecksumVerbose { - pub fn new(status: bool, quiet: bool, warn: bool) -> Self { - use ChecksumVerbose::*; - - // Assume only one of the three booleans will be enabled at once. - // This is ensured by clap's overriding arguments. - match (status, quiet, warn) { - (true, _, _) => Status, - (_, true, _) => Quiet, - (_, _, true) => Warning, - _ => Normal, - } - } - - #[inline] - pub fn over_status(self) -> bool { - self > Self::Status - } - - #[inline] - pub fn over_quiet(self) -> bool { - self > Self::Quiet - } - - #[inline] - pub fn at_least_warning(self) -> bool { - self >= Self::Warning - } -} - -/// This struct regroups CLI flags. -#[derive(Debug, Default, Clone, Copy)] -pub struct ChecksumOptions { - pub binary: bool, - pub ignore_missing: bool, - pub strict: bool, - pub verbose: ChecksumVerbose, -} - -#[derive(Debug, Error)] -pub enum ChecksumError { - #[error("the --raw option is not supported with multiple files")] - RawMultipleFiles, - #[error("the --ignore-missing option is meaningful only when verifying checksums")] - IgnoreNotCheck, - #[error("the --strict option is meaningful only when verifying checksums")] - StrictNotCheck, - #[error("the --quiet option is meaningful only when verifying checksums")] - QuietNotCheck, - - // --length sanitization errors - #[error("--length required for {}", .0.quote())] - LengthRequired(String), - #[error("invalid length: {}", .0.quote())] - InvalidLength(String), - #[error("maximum digest length for {} is 512 bits", .0.quote())] - LengthTooBigForBlake(String), - #[error("length is not a multiple of 8")] - LengthNotMultipleOf8, - #[error("digest length for {} must be 224, 256, 384, or 512", .0.quote())] - InvalidLengthForSha(String), - #[error("--algorithm={0} requires specifying --length 224, 256, 384, or 512")] - LengthRequiredForSha(String), - #[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}}")] - AlgorithmNotSupportedWithCheck, - #[error("You cannot combine multiple hash algorithms!")] - CombineMultipleAlgorithms, - #[error("Needs an algorithm to hash with.\nUse --help for more information.")] - NeedAlgorithmToHash, - #[error("unknown algorithm: {0}: clap should have prevented this case")] - UnknownAlgorithm(String), - #[error("")] - Io(#[from] io::Error), -} - -impl UError for ChecksumError { - fn code(&self) -> i32 { - 1 - } -} - #[allow(clippy::comparison_chain)] fn print_cksum_report(res: &ChecksumResult) { if res.bad_format == 1 { @@ -899,16 +497,16 @@ fn get_expected_digest_as_hex_string( /// Returns a reader that reads from the specified file, or from stdin if `filename_to_check` is "-". fn get_file_to_check( filename: &OsStr, - opts: ChecksumOptions, + opts: ChecksumValidateOptions, ) -> Result, LineCheckError> { - let filename_bytes = os_str_as_bytes(filename).expect("UTF-8 error"); + let filename_bytes = os_str_as_bytes(filename).map_err(|e| LineCheckError::UError(e.into()))?; if filename == "-" { - Ok(Box::new(stdin())) // Use stdin if "-" is specified in the checksum file + Ok(Box::new(io::stdin())) // Use stdin if "-" is specified in the checksum file } else { let failed_open = || { print_file_report( - std::io::stdout(), + io::stdout(), filename_bytes, FileChecksumResult::CantOpen, "", @@ -1035,7 +633,7 @@ fn compute_and_check_digest_from_file( filename: &[u8], expected_checksum: &str, algo: SizedAlgoKind, - opts: ChecksumOptions, + opts: ChecksumValidateOptions, ) -> Result<(), LineCheckError> { let (filename_to_check_unescaped, prefix) = unescape_filename(filename); let real_filename_to_check = os_str_from_bytes(&filename_to_check_unescaped)?; @@ -1047,7 +645,7 @@ fn compute_and_check_digest_from_file( // Read the file and calculate the checksum let mut digest = algo.create_digest(); let (calculated_checksum, _) = - digest_reader(&mut digest, &mut file_reader, opts.binary, algo.bitlen()).unwrap(); + digest_reader(&mut digest, &mut file_reader, false, algo.bitlen()).unwrap(); // Do the checksum validation let checksum_correct = expected_checksum == calculated_checksum; @@ -1070,7 +668,7 @@ fn compute_and_check_digest_from_file( fn process_algo_based_line( line_info: &LineInfo, cli_algo_kind: Option, - opts: ChecksumOptions, + opts: ChecksumValidateOptions, last_algo: &mut Option, ) -> Result<(), LineCheckError> { let filename_to_check = line_info.filename.as_slice(); @@ -1099,7 +697,7 @@ fn process_non_algo_based_line( line_info: &LineInfo, cli_algo_kind: AlgoKind, cli_algo_length: Option, - opts: ChecksumOptions, + opts: ChecksumValidateOptions, ) -> Result<(), LineCheckError> { let mut filename_to_check = line_info.filename.as_slice(); if filename_to_check.starts_with(b"*") @@ -1145,7 +743,7 @@ fn process_checksum_line( i: usize, cli_algo_name: Option, cli_algo_length: Option, - opts: ChecksumOptions, + opts: ChecksumValidateOptions, cached_line_format: &mut Option, last_algo: &mut Option, ) -> Result<(), LineCheckError> { @@ -1178,7 +776,7 @@ fn process_checksum_file( filename_input: &OsStr, cli_algo_kind: Option, cli_algo_length: Option, - opts: ChecksumOptions, + opts: ChecksumValidateOptions, ) -> Result<(), FileCheckError> { let mut res = ChecksumResult::default(); @@ -1308,7 +906,7 @@ pub fn perform_checksum_validation<'a, I>( files: I, algo_kind: Option, length_input: Option, - opts: ChecksumOptions, + opts: ChecksumValidateOptions, ) -> UResult<()> where I: Iterator, @@ -1332,201 +930,11 @@ where } } -pub fn digest_reader( - digest: &mut Box, - reader: &mut T, - binary: bool, - output_bits: usize, -) -> io::Result<(String, usize)> { - digest.reset(); - - // Read bytes from `reader` and write those bytes to `digest`. - // - // If `binary` is `false` and the operating system is Windows, then - // `DigestWriter` replaces "\r\n" with "\n" before it writes the - // bytes into `digest`. Otherwise, it just inserts the bytes as-is. - // - // In order to support replacing "\r\n", we must call `finalize()` - // in order to support the possibility that the last character read - // from the reader was "\r". (This character gets buffered by - // `DigestWriter` and only written if the following character is - // "\n". But when "\r" is the last character read, we need to force - // it to be written.) - let mut digest_writer = DigestWriter::new(digest, binary); - let output_size = std::io::copy(reader, &mut digest_writer)? as usize; - digest_writer.finalize(); - - if digest.output_bits() > 0 { - Ok((digest.result_str(), output_size)) - } else { - // Assume it's SHAKE. result_str() doesn't work with shake (as of 8/30/2016) - let mut bytes = vec![0; output_bits.div_ceil(8)]; - digest.hash_finalize(&mut bytes); - Ok((hex::encode(bytes), output_size)) - } -} - -/// Calculates the length of the digest. -pub fn calculate_blake2b_length(bit_length: usize) -> UResult> { - calculate_blake2b_length_str(bit_length.to_string().as_str()) -} - -/// Calculates the length of the digest. -pub fn calculate_blake2b_length_str(bit_length: &str) -> UResult> { - // Blake2b's length is parsed in an u64. - match bit_length.parse::() { - Ok(0) => Ok(None), - - // Error cases - Ok(n) if n > 512 => { - show_error!("{}", ChecksumError::InvalidLength(bit_length.into())); - Err(ChecksumError::LengthTooBigForBlake("BLAKE2b".into()).into()) - } - Err(e) if *e.kind() == IntErrorKind::PosOverflow => { - show_error!("{}", ChecksumError::InvalidLength(bit_length.into())); - Err(ChecksumError::LengthTooBigForBlake("BLAKE2b".into()).into()) - } - Err(_) => Err(ChecksumError::InvalidLength(bit_length.into()).into()), - - Ok(n) if n % 8 != 0 => { - show_error!("{}", ChecksumError::InvalidLength(bit_length.into())); - Err(ChecksumError::LengthNotMultipleOf8.into()) - } - - // Valid cases - - // When length is 512, it is blake2b's default. So, don't show it - Ok(512) => Ok(None), - // Divide by 8, as our blake2b implementation expects bytes instead of bits. - Ok(n) => Ok(Some(n / 8)), - } -} - -pub fn validate_sha2_sha3_length(algo_name: AlgoKind, length: Option) -> UResult { - match length { - Some(224) => Ok(ShaLength::Len224), - Some(256) => Ok(ShaLength::Len256), - Some(384) => Ok(ShaLength::Len384), - Some(512) => Ok(ShaLength::Len512), - Some(len) => { - show_error!("{}", ChecksumError::InvalidLength(len.to_string())); - Err(ChecksumError::InvalidLengthForSha(algo_name.to_uppercase().into()).into()) - } - None => Err(ChecksumError::LengthRequiredForSha(algo_name.to_lowercase().into()).into()), - } -} - -pub fn sanitize_sha2_sha3_length_str(algo_kind: AlgoKind, length: &str) -> UResult { - // There is a difference in the errors sent when the length is not a number - // vs. its an invalid number. - // - // When inputting an invalid number, an extra error message it printed to - // remind of the accepted inputs. - let len = match length.parse::() { - Ok(l) => l, - // Note: Positive overflow while parsing counts as an invalid number, - // but a number still. - Err(e) if *e.kind() == IntErrorKind::PosOverflow => { - show_error!("{}", ChecksumError::InvalidLength(length.into())); - return Err(ChecksumError::InvalidLengthForSha(algo_kind.to_uppercase().into()).into()); - } - Err(_) => return Err(ChecksumError::InvalidLength(length.into()).into()), - }; - - if [224, 256, 384, 512].contains(&len) { - Ok(len) - } else { - show_error!("{}", ChecksumError::InvalidLength(length.into())); - Err(ChecksumError::InvalidLengthForSha(algo_kind.to_uppercase().into()).into()) - } -} - -pub fn unescape_filename(filename: &[u8]) -> (Vec, &'static str) { - let mut unescaped = Vec::with_capacity(filename.len()); - let mut byte_iter = filename.iter().peekable(); - loop { - let Some(byte) = byte_iter.next() else { - break; - }; - if *byte == b'\\' { - match byte_iter.next() { - Some(b'\\') => unescaped.push(b'\\'), - Some(b'n') => unescaped.push(b'\n'), - Some(b'r') => unescaped.push(b'\r'), - Some(x) => { - unescaped.push(b'\\'); - unescaped.push(*x); - } - _ => {} - } - } else { - unescaped.push(*byte); - } - } - let prefix = if unescaped == filename { "" } else { "\\" }; - (unescaped, prefix) -} - -pub fn escape_filename(filename: &Path) -> (String, &'static str) { - let original = filename.as_os_str().to_string_lossy(); - let escaped = original - .replace('\\', "\\\\") - .replace('\n', "\\n") - .replace('\r', "\\r"); - let prefix = if escaped == original { "" } else { "\\" }; - (escaped, prefix) -} - #[cfg(test)] mod tests { - use super::*; use std::ffi::OsString; - #[test] - fn test_unescape_filename() { - let (unescaped, prefix) = unescape_filename(b"test\\nfile.txt"); - assert_eq!(unescaped, b"test\nfile.txt"); - assert_eq!(prefix, "\\"); - let (unescaped, prefix) = unescape_filename(b"test\\nfile.txt"); - assert_eq!(unescaped, b"test\nfile.txt"); - assert_eq!(prefix, "\\"); - - let (unescaped, prefix) = unescape_filename(b"test\\rfile.txt"); - assert_eq!(unescaped, b"test\rfile.txt"); - assert_eq!(prefix, "\\"); - - let (unescaped, prefix) = unescape_filename(b"test\\\\file.txt"); - assert_eq!(unescaped, b"test\\file.txt"); - assert_eq!(prefix, "\\"); - } - - #[test] - fn test_escape_filename() { - let (escaped, prefix) = escape_filename(Path::new("testfile.txt")); - assert_eq!(escaped, "testfile.txt"); - assert_eq!(prefix, ""); - - let (escaped, prefix) = escape_filename(Path::new("test\nfile.txt")); - assert_eq!(escaped, "test\\nfile.txt"); - assert_eq!(prefix, "\\"); - - let (escaped, prefix) = escape_filename(Path::new("test\rfile.txt")); - assert_eq!(escaped, "test\\rfile.txt"); - assert_eq!(prefix, "\\"); - - let (escaped, prefix) = escape_filename(Path::new("test\\file.txt")); - assert_eq!(escaped, "test\\\\file.txt"); - assert_eq!(prefix, "\\"); - } - - #[test] - fn test_calculate_blake2b_length() { - assert_eq!(calculate_blake2b_length(0).unwrap(), None); - assert!(calculate_blake2b_length(10).is_err()); - assert!(calculate_blake2b_length(520).is_err()); - assert_eq!(calculate_blake2b_length(512).unwrap(), None); - assert_eq!(calculate_blake2b_length(256).unwrap(), Some(32)); - } + use super::*; #[test] fn test_algo_based_parser() { @@ -1769,7 +1177,7 @@ mod tests { #[test] fn test_print_file_report() { - let opts = ChecksumOptions::default(); + let opts = ChecksumValidateOptions::default(); let cases: &[(&[u8], FileChecksumResult, &str, &[u8])] = &[ (b"filename", FileChecksumResult::Ok, "", b"filename: OK\n"), From 3849d91d69f00652ec2e6cf51d29f931ebb34d72 Mon Sep 17 00:00:00 2001 From: Dorian Peron Date: Thu, 6 Nov 2025 16:42:04 +0100 Subject: [PATCH 076/182] checksum: Move cksum computation to uucore::checksum --- src/uu/cksum/src/cksum.rs | 262 +----------------- .../src/lib/features/checksum/compute.rs | 246 ++++++++++++++++ src/uucore/src/lib/features/checksum/mod.rs | 1 + 3 files changed, 259 insertions(+), 250 deletions(-) create mode 100644 src/uucore/src/lib/features/checksum/compute.rs diff --git a/src/uu/cksum/src/cksum.rs b/src/uu/cksum/src/cksum.rs index a0a5c00df..0a246b8e9 100644 --- a/src/uu/cksum/src/cksum.rs +++ b/src/uu/cksum/src/cksum.rs @@ -8,257 +8,20 @@ use clap::builder::ValueParser; use clap::{Arg, ArgAction, Command}; use std::ffi::{OsStr, OsString}; -use std::fs::File; -use std::io::{BufReader, Read, Write, stdin, stdout}; use std::iter; -use std::path::Path; -use uucore::checksum::validate::{ChecksumOptions, ChecksumVerbose, perform_checksum_validation}; +use uucore::checksum::compute::{ + ChecksumComputeOptions, DigestFormat, OutputFormat, ReadingMode, perform_checksum_computation, +}; +use uucore::checksum::validate::{ + ChecksumValidateOptions, ChecksumVerbose, perform_checksum_validation, +}; use uucore::checksum::{ AlgoKind, ChecksumError, SUPPORTED_ALGORITHMS, SizedAlgoKind, calculate_blake2b_length_str, - digest_reader, sanitize_sha2_sha3_length_str, + sanitize_sha2_sha3_length_str, }; -use uucore::translate; - -use uucore::{ - encoding, - error::{FromIo, UResult, USimpleError}, - format_usage, - line_ending::LineEnding, - os_str_as_bytes, show, - sum::Digest, -}; - -struct Options { - algo_kind: SizedAlgoKind, - digest: Box, - output_format: OutputFormat, - line_ending: LineEnding, -} - -/// Reading mode used to compute digest. -/// -/// On most linux systems, this is irrelevant, as there is no distinction -/// between text and binary files. Refer to GNU's cksum documentation for more -/// information. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum ReadingMode { - Binary, - Text, -} - -impl ReadingMode { - #[inline] - fn as_char(&self) -> char { - match self { - Self::Binary => '*', - Self::Text => ' ', - } - } -} - -/// Whether to write the digest as hexadecimal or encoded in base64. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum DigestFormat { - Hexadecimal, - Base64, -} - -impl DigestFormat { - #[inline] - fn is_base64(&self) -> bool { - *self == Self::Base64 - } -} - -/// Holds the representation that shall be used for printing a checksum line -#[derive(Debug, PartialEq, Eq)] -enum OutputFormat { - /// Raw digest - Raw, - - /// Selected for older algorithms which had their custom formatting - /// - /// Default for crc, sysv, bsd - Legacy, - - /// `$ALGO_NAME ($FILENAME) = $DIGEST` - Tagged(DigestFormat), - - /// '$DIGEST $FLAG$FILENAME' - /// where 'flag' depends on the reading mode - /// - /// Default for standalone checksum utilities - Untagged(DigestFormat, ReadingMode), -} - -impl OutputFormat { - #[inline] - fn is_raw(&self) -> bool { - *self == Self::Raw - } -} - -fn print_legacy_checksum( - options: &Options, - filename: &OsStr, - sum: &str, - size: usize, -) -> UResult<()> { - debug_assert!(options.algo_kind.is_legacy()); - - // Print the sum - match options.algo_kind { - SizedAlgoKind::Sysv => print!( - "{} {}", - sum.parse::().unwrap(), - size.div_ceil(options.algo_kind.bitlen()), - ), - SizedAlgoKind::Bsd => { - // The BSD checksum output is 5 digit integer - let bsd_width = 5; - print!( - "{:0bsd_width$} {:bsd_width$}", - sum.parse::().unwrap(), - size.div_ceil(options.algo_kind.bitlen()), - ); - } - SizedAlgoKind::Crc | SizedAlgoKind::Crc32b => { - print!("{sum} {size}"); - } - _ => unreachable!("Not a legacy algorithm"), - } - - // Print the filename after a space if not stdin - if filename != "-" { - print!(" "); - let _dropped_result = stdout().write_all(os_str_as_bytes(filename)?); - } - - Ok(()) -} - -fn print_tagged_checksum(options: &Options, filename: &OsStr, sum: &String) -> UResult<()> { - // Print algo name and opening parenthesis. - print!("{} (", options.algo_kind.to_tag()); - - // Print filename - let _dropped_result = stdout().write_all(os_str_as_bytes(filename)?); - - // Print closing parenthesis and sum - print!(") = {sum}"); - - Ok(()) -} - -fn print_untagged_checksum( - filename: &OsStr, - sum: &String, - reading_mode: ReadingMode, -) -> UResult<()> { - // Print checksum and reading mode flag - print!("{sum} {}", reading_mode.as_char()); - - // Print filename - let _dropped_result = stdout().write_all(os_str_as_bytes(filename)?); - - Ok(()) -} - -/// Calculate checksum -/// -/// # Arguments -/// -/// * `options` - CLI options for the assigning checksum algorithm -/// * `files` - A iterator of [`OsStr`] which is a bunch of files that are using for calculating checksum -fn cksum<'a, I>(mut options: Options, files: I) -> UResult<()> -where - I: Iterator, -{ - let mut files = files.peekable(); - - while let Some(filename) = files.next() { - // Check that in raw mode, we are not provided with several files. - if options.output_format.is_raw() && files.peek().is_some() { - return Err(Box::new(ChecksumError::RawMultipleFiles)); - } - - let filepath = Path::new(filename); - let stdin_buf; - let file_buf; - if filepath.is_dir() { - show!(USimpleError::new( - 1, - translate!("cksum-error-is-directory", "file" => filepath.display()) - )); - continue; - } - - // Handle the file input - let mut file = BufReader::new(if filename == "-" { - stdin_buf = stdin(); - Box::new(stdin_buf) as Box - } else { - file_buf = match File::open(filepath) { - Ok(file) => file, - Err(err) => { - show!(err.map_err_context(|| filepath.to_string_lossy().to_string())); - continue; - } - }; - Box::new(file_buf) as Box - }); - - let (sum_hex, sz) = digest_reader( - &mut options.digest, - &mut file, - false, - options.algo_kind.bitlen(), - ) - .map_err_context(|| translate!("cksum-error-failed-to-read-input"))?; - - // Encodes the sum if df is Base64, leaves as-is otherwise. - let encode_sum = |sum: String, df: DigestFormat| { - if df.is_base64() { - encoding::for_cksum::BASE64.encode(&hex::decode(sum).unwrap()) - } else { - sum - } - }; - - match options.output_format { - OutputFormat::Raw => { - let bytes = match options.algo_kind { - SizedAlgoKind::Crc | SizedAlgoKind::Crc32b => { - sum_hex.parse::().unwrap().to_be_bytes().to_vec() - } - SizedAlgoKind::Sysv | SizedAlgoKind::Bsd => { - sum_hex.parse::().unwrap().to_be_bytes().to_vec() - } - _ => hex::decode(sum_hex).unwrap(), - }; - // Cannot handle multiple files anyway, output immediately. - stdout().write_all(&bytes)?; - return Ok(()); - } - OutputFormat::Legacy => { - print_legacy_checksum(&options, filename, &sum_hex, sz)?; - } - OutputFormat::Tagged(digest_format) => { - print_tagged_checksum(&options, filename, &encode_sum(sum_hex, digest_format))?; - } - OutputFormat::Untagged(digest_format, reading_mode) => { - print_untagged_checksum( - filename, - &encode_sum(sum_hex, digest_format), - reading_mode, - )?; - } - } - - print!("{}", options.line_ending); - } - Ok(()) -} +use uucore::error::UResult; +use uucore::line_ending::LineEnding; +use uucore::{format_usage, translate}; mod options { pub const ALGORITHM: &str = "algorithm"; @@ -455,14 +218,13 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { matches.get_flag(options::BASE64), ); - let opts = Options { + let opts = ChecksumComputeOptions { algo_kind: algo, - digest: algo.create_digest(), output_format, line_ending, }; - cksum(opts, files)?; + perform_checksum_computation(opts, files)?; Ok(()) } diff --git a/src/uucore/src/lib/features/checksum/compute.rs b/src/uucore/src/lib/features/checksum/compute.rs new file mode 100644 index 000000000..015e9bb0f --- /dev/null +++ b/src/uucore/src/lib/features/checksum/compute.rs @@ -0,0 +1,246 @@ +use std::ffi::OsStr; +use std::fs::File; +use std::io::{self, BufReader, Read, Write}; +use std::path::Path; + +use crate::checksum::{ChecksumError, SizedAlgoKind, digest_reader}; +use crate::error::{FromIo, UResult, USimpleError}; +use crate::line_ending::LineEnding; +use crate::{encoding, os_str_as_bytes, show, translate}; + +pub struct ChecksumComputeOptions { + pub algo_kind: SizedAlgoKind, + pub output_format: OutputFormat, + pub line_ending: LineEnding, +} + +/// Reading mode used to compute digest. +/// +/// On most linux systems, this is irrelevant, as there is no distinction +/// between text and binary files. Refer to GNU's cksum documentation for more +/// information. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ReadingMode { + Binary, + Text, +} + +impl ReadingMode { + #[inline] + fn as_char(&self) -> char { + match self { + Self::Binary => '*', + Self::Text => ' ', + } + } +} + +/// Whether to write the digest as hexadecimal or encoded in base64. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DigestFormat { + Hexadecimal, + Base64, +} + +impl DigestFormat { + #[inline] + fn is_base64(&self) -> bool { + *self == Self::Base64 + } +} + +/// Holds the representation that shall be used for printing a checksum line +#[derive(Debug, PartialEq, Eq)] +pub enum OutputFormat { + /// Raw digest + Raw, + + /// Selected for older algorithms which had their custom formatting + /// + /// Default for crc, sysv, bsd + Legacy, + + /// `$ALGO_NAME ($FILENAME) = $DIGEST` + Tagged(DigestFormat), + + /// '$DIGEST $FLAG$FILENAME' + /// where 'flag' depends on the reading mode + /// + /// Default for standalone checksum utilities + Untagged(DigestFormat, ReadingMode), +} + +impl OutputFormat { + #[inline] + fn is_raw(&self) -> bool { + *self == Self::Raw + } +} + +fn print_legacy_checksum( + options: &ChecksumComputeOptions, + filename: &OsStr, + sum: &str, + size: usize, +) -> UResult<()> { + debug_assert!(options.algo_kind.is_legacy()); + + // Print the sum + match options.algo_kind { + SizedAlgoKind::Sysv => print!( + "{} {}", + sum.parse::().unwrap(), + size.div_ceil(options.algo_kind.bitlen()), + ), + SizedAlgoKind::Bsd => { + // The BSD checksum output is 5 digit integer + let bsd_width = 5; + print!( + "{:0bsd_width$} {:bsd_width$}", + sum.parse::().unwrap(), + size.div_ceil(options.algo_kind.bitlen()), + ); + } + SizedAlgoKind::Crc | SizedAlgoKind::Crc32b => { + print!("{sum} {size}"); + } + _ => unreachable!("Not a legacy algorithm"), + } + + // Print the filename after a space if not stdin + if filename != "-" { + print!(" "); + let _dropped_result = io::stdout().write_all(os_str_as_bytes(filename)?); + } + + Ok(()) +} + +fn print_tagged_checksum( + options: &ChecksumComputeOptions, + filename: &OsStr, + sum: &String, +) -> UResult<()> { + // Print algo name and opening parenthesis. + print!("{} (", options.algo_kind.to_tag()); + + // Print filename + let _dropped_result = io::stdout().write_all(os_str_as_bytes(filename)?); + + // Print closing parenthesis and sum + print!(") = {sum}"); + + Ok(()) +} + +fn print_untagged_checksum( + filename: &OsStr, + sum: &String, + reading_mode: ReadingMode, +) -> UResult<()> { + // Print checksum and reading mode flag + print!("{sum} {}", reading_mode.as_char()); + + // Print filename + let _dropped_result = io::stdout().write_all(os_str_as_bytes(filename)?); + + Ok(()) +} + +/// Calculate checksum +/// +/// # Arguments +/// +/// * `options` - CLI options for the assigning checksum algorithm +/// * `files` - A iterator of [`OsStr`] which is a bunch of files that are using for calculating checksum +pub fn perform_checksum_computation<'a, I>(options: ChecksumComputeOptions, files: I) -> UResult<()> +where + I: Iterator, +{ + let mut files = files.peekable(); + + while let Some(filename) = files.next() { + // Check that in raw mode, we are not provided with several files. + if options.output_format.is_raw() && files.peek().is_some() { + return Err(Box::new(ChecksumError::RawMultipleFiles)); + } + + let filepath = Path::new(filename); + let stdin_buf; + let file_buf; + if filepath.is_dir() { + show!(USimpleError::new( + 1, + translate!("cksum-error-is-directory", "file" => filepath.display()) + )); + continue; + } + + // Handle the file input + let mut file = BufReader::new(if filename == "-" { + stdin_buf = io::stdin(); + Box::new(stdin_buf) as Box + } else { + file_buf = match File::open(filepath) { + Ok(file) => file, + Err(err) => { + show!(err.map_err_context(|| filepath.to_string_lossy().to_string())); + continue; + } + }; + Box::new(file_buf) as Box + }); + + let mut digest = options.algo_kind.create_digest(); + + let (sum_hex, sz) = digest_reader( + &mut digest, + &mut file, + false, + options.algo_kind.bitlen(), + ) + .map_err_context(|| translate!("cksum-error-failed-to-read-input"))?; + + // Encodes the sum if df is Base64, leaves as-is otherwise. + let encode_sum = |sum: String, df: DigestFormat| { + if df.is_base64() { + encoding::for_cksum::BASE64.encode(&hex::decode(sum).unwrap()) + } else { + sum + } + }; + + match options.output_format { + OutputFormat::Raw => { + let bytes = match options.algo_kind { + SizedAlgoKind::Crc | SizedAlgoKind::Crc32b => { + sum_hex.parse::().unwrap().to_be_bytes().to_vec() + } + SizedAlgoKind::Sysv | SizedAlgoKind::Bsd => { + sum_hex.parse::().unwrap().to_be_bytes().to_vec() + } + _ => hex::decode(sum_hex).unwrap(), + }; + // Cannot handle multiple files anyway, output immediately. + io::stdout().write_all(&bytes)?; + return Ok(()); + } + OutputFormat::Legacy => { + print_legacy_checksum(&options, filename, &sum_hex, sz)?; + } + OutputFormat::Tagged(digest_format) => { + print_tagged_checksum(&options, filename, &encode_sum(sum_hex, digest_format))?; + } + OutputFormat::Untagged(digest_format, reading_mode) => { + print_untagged_checksum( + filename, + &encode_sum(sum_hex, digest_format), + reading_mode, + )?; + } + } + + print!("{}", options.line_ending); + } + Ok(()) +} diff --git a/src/uucore/src/lib/features/checksum/mod.rs b/src/uucore/src/lib/features/checksum/mod.rs index 29af7a491..a3b7e53c9 100644 --- a/src/uucore/src/lib/features/checksum/mod.rs +++ b/src/uucore/src/lib/features/checksum/mod.rs @@ -21,6 +21,7 @@ use crate::{ }; use thiserror::Error; +pub mod compute; pub mod validate; pub const ALGORITHM_OPTIONS_SYSV: &str = "sysv"; From c97ce1bb8e08cbe891027ae72343d6310bdd8f05 Mon Sep 17 00:00:00 2001 From: Dorian Peron Date: Thu, 6 Nov 2025 18:15:34 +0100 Subject: [PATCH 077/182] checksum: Adapt checksum computation to hashsum --- src/uu/cksum/src/cksum.rs | 40 +---- src/uu/hashsum/Cargo.toml | 2 +- src/uu/hashsum/src/hashsum.rs | 154 ++++-------------- .../src/lib/features/checksum/compute.rs | 133 ++++++++++++--- src/uucore/src/lib/features/checksum/mod.rs | 40 +++-- 5 files changed, 165 insertions(+), 204 deletions(-) diff --git a/src/uu/cksum/src/cksum.rs b/src/uu/cksum/src/cksum.rs index 0a246b8e9..7ff0243eb 100644 --- a/src/uu/cksum/src/cksum.rs +++ b/src/uu/cksum/src/cksum.rs @@ -10,7 +10,7 @@ use clap::{Arg, ArgAction, Command}; use std::ffi::{OsStr, OsString}; use std::iter; use uucore::checksum::compute::{ - ChecksumComputeOptions, DigestFormat, OutputFormat, ReadingMode, perform_checksum_computation, + ChecksumComputeOptions, figure_out_output_format, perform_checksum_computation, }; use uucore::checksum::validate::{ ChecksumValidateOptions, ChecksumVerbose, perform_checksum_validation, @@ -84,43 +84,6 @@ fn handle_tag_text_binary_flags>( Ok((tag, binary)) } -/// Use already-processed arguments to decide the output format. -fn figure_out_output_format( - algo: SizedAlgoKind, - tag: bool, - binary: bool, - raw: bool, - base64: bool, -) -> OutputFormat { - // Raw output format takes precedence over anything else. - if raw { - return OutputFormat::Raw; - } - - // Then, if the algo is legacy, takes precedence over the rest - if algo.is_legacy() { - return OutputFormat::Legacy; - } - - let digest_format = if base64 { - DigestFormat::Base64 - } else { - DigestFormat::Hexadecimal - }; - - // After that, decide between tagged and untagged output - if tag { - OutputFormat::Tagged(digest_format) - } else { - let reading_mode = if binary { - ReadingMode::Binary - } else { - ReadingMode::Text - }; - OutputFormat::Untagged(digest_format, reading_mode) - } -} - /// Sanitize the `--length` argument depending on `--algorithm` and `--length`. fn maybe_sanitize_length( algo_cli: Option, @@ -222,6 +185,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { algo_kind: algo, output_format, line_ending, + no_names: false, }; perform_checksum_computation(opts, files)?; diff --git a/src/uu/hashsum/Cargo.toml b/src/uu/hashsum/Cargo.toml index 00eb152ed..ec382870b 100644 --- a/src/uu/hashsum/Cargo.toml +++ b/src/uu/hashsum/Cargo.toml @@ -19,7 +19,7 @@ path = "src/hashsum.rs" [dependencies] clap = { workspace = true } -uucore = { workspace = true, features = ["checksum", "sum"] } +uucore = { workspace = true, features = ["checksum", "encoding", "sum"] } fluent = { workspace = true } [[bin]] diff --git a/src/uu/hashsum/src/hashsum.rs b/src/uu/hashsum/src/hashsum.rs index 61bd0f0ff..ebe3ac033 100644 --- a/src/uu/hashsum/src/hashsum.rs +++ b/src/uu/hashsum/src/hashsum.rs @@ -5,47 +5,26 @@ // spell-checker:ignore (ToDO) algo, algoname, bitlen, regexes, nread, nonames -use clap::ArgAction; -use clap::builder::ValueParser; -use clap::value_parser; -use clap::{Arg, ArgMatches, Command}; use std::ffi::{OsStr, OsString}; -use std::fs::File; -use std::io::{BufReader, Read, stdin}; use std::iter; use std::num::ParseIntError; use std::path::Path; +use clap::builder::ValueParser; +use clap::{Arg, ArgAction, ArgMatches, Command, value_parser}; + +use uucore::checksum::compute::{ + ChecksumComputeOptions, figure_out_output_format, perform_checksum_computation, +}; use uucore::checksum::validate::{ ChecksumValidateOptions, ChecksumVerbose, perform_checksum_validation, }; -use uucore::checksum::{ - AlgoKind, ChecksumError, SizedAlgoKind, calculate_blake2b_length, digest_reader, - escape_filename, -}; -use uucore::error::{UResult, strip_errno}; -use uucore::sum::Digest; +use uucore::checksum::{AlgoKind, ChecksumError, SizedAlgoKind, calculate_blake2b_length}; +use uucore::error::UResult; +use uucore::line_ending::LineEnding; use uucore::{format_usage, translate}; const NAME: &str = "hashsum"; -// Using the same read buffer size as GNU -const READ_BUFFER_SIZE: usize = 32 * 1024; - -struct Options<'a> { - algo: SizedAlgoKind, - digest: Box, - binary: bool, - binary_name: &'a str, - //check: bool, - tag: bool, - nonames: bool, - //status: bool, - //quiet: bool, - //strict: bool, - //warn: bool, - zero: bool, - //ignore_missing: bool, -} /// Creates a hasher instance based on the command-line flags. /// @@ -186,9 +165,9 @@ pub fn uumain(mut args: impl uucore::Args) -> UResult<()> { }; let check = matches.get_flag("check"); let status = matches.get_flag("status"); - let quiet = matches.get_flag("quiet") || status; + let quiet = matches.get_flag("quiet"); let strict = matches.get_flag("strict"); - let warn = matches.get_flag("warn") && !status; + let warn = matches.get_flag("warn"); let ignore_missing = matches.get_flag("ignore-missing"); if ignore_missing && !check { @@ -232,33 +211,36 @@ pub fn uumain(mut args: impl uucore::Args) -> UResult<()> { return Err(ChecksumError::StrictNotCheck.into()); } - let nonames = *matches + let no_names = *matches .try_get_one("no-names") .unwrap_or(None) .unwrap_or(&false); - let zero = matches.get_flag("zero"); + let line_ending = LineEnding::from_zero_flag(matches.get_flag("zero")); let algo = SizedAlgoKind::from_unsized(algo_kind, length)?; - let opts = Options { - algo, - digest: algo.create_digest(), - binary, - binary_name: &binary_name, - tag: matches.get_flag("tag"), - nonames, - //status, - //quiet, - //warn, - zero, - //ignore_missing, + let opts = ChecksumComputeOptions { + algo_kind: algo, + output_format: figure_out_output_format( + algo, + matches.get_flag(options::TAG), + binary, + /* raw */ false, + /* base64: */ false, + ), + line_ending, + no_names, }; + let files = matches.get_many::(options::FILE).map_or_else( + // No files given, read from stdin. + || Box::new(iter::once(OsStr::new("-"))) as Box>, + // At least one file given, read from them. + |files| Box::new(files.map(OsStr::new)) as Box>, + ); + // Show the hashsum of the input - match matches.get_many::(options::FILE) { - Some(files) => hashsum(opts, files.map(|f| f.as_os_str())), - None => hashsum(opts, iter::once(OsStr::new("-"))), - } + perform_checksum_computation(opts, files) } mod options { @@ -489,75 +471,3 @@ fn uu_app(binary_name: &str) -> (Command, bool) { (command, is_hashsum_bin) } - -#[allow(clippy::cognitive_complexity)] -fn hashsum<'a, I>(mut options: Options, files: I) -> UResult<()> -where - I: Iterator, -{ - let binary_marker = if options.binary { "*" } else { " " }; - let mut err_found = None; - for filename in files { - let filename = Path::new(filename); - - let mut file = BufReader::with_capacity( - READ_BUFFER_SIZE, - if filename == OsStr::new("-") { - Box::new(stdin()) as Box - } else { - let file_buf = match File::open(filename) { - Ok(f) => f, - Err(e) => { - eprintln!( - "{}: {}: {}", - options.binary_name, - filename.to_string_lossy(), - strip_errno(&e) - ); - err_found = Some(ChecksumError::Io(e)); - continue; - } - }; - Box::new(file_buf) as Box - }, - ); - - let sum = match digest_reader( - &mut options.digest, - &mut file, - options.binary, - options.algo.bitlen(), - ) { - Ok((sum, _)) => sum, - Err(e) => { - eprintln!( - "{}: {}: {}", - options.binary_name, - filename.to_string_lossy(), - strip_errno(&e) - ); - err_found = Some(ChecksumError::Io(e)); - continue; - } - }; - - let (escaped_filename, prefix) = escape_filename(filename); - if options.tag { - println!( - "{prefix}{} ({escaped_filename}) = {sum}", - options.algo.to_tag() - ); - } else if options.nonames { - println!("{sum}"); - } else if options.zero { - // with zero, we don't escape the filename - print!("{sum} {binary_marker}{}\0", filename.display()); - } else { - println!("{prefix}{sum} {binary_marker}{escaped_filename}"); - } - } - match err_found { - None => Ok(()), - Some(e) => Err(Box::new(e)), - } -} diff --git a/src/uucore/src/lib/features/checksum/compute.rs b/src/uucore/src/lib/features/checksum/compute.rs index 015e9bb0f..426b2633e 100644 --- a/src/uucore/src/lib/features/checksum/compute.rs +++ b/src/uucore/src/lib/features/checksum/compute.rs @@ -1,17 +1,36 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +// spell-checker:ignore bitlen + use std::ffi::OsStr; use std::fs::File; use std::io::{self, BufReader, Read, Write}; use std::path::Path; -use crate::checksum::{ChecksumError, SizedAlgoKind, digest_reader}; +use crate::checksum::{ChecksumError, SizedAlgoKind, digest_reader, escape_filename}; use crate::error::{FromIo, UResult, USimpleError}; use crate::line_ending::LineEnding; -use crate::{encoding, os_str_as_bytes, show, translate}; +use crate::{encoding, show, translate}; + +/// Use the same buffer size as GNU when reading a file to create a checksum +/// from it: 32 KiB. +const READ_BUFFER_SIZE: usize = 32 * 1024; pub struct ChecksumComputeOptions { + /// Which algorithm to use to compute the digest. pub algo_kind: SizedAlgoKind, + + /// Printing format to use for each checksum. pub output_format: OutputFormat, + + /// Whether to finish lines with '\n' or '\0'. pub line_ending: LineEnding, + + /// (non-GNU option) Do not print file names + pub no_names: bool, } /// Reading mode used to compute digest. @@ -77,6 +96,43 @@ impl OutputFormat { } } +/// Use already-processed arguments to decide the output format. +pub fn figure_out_output_format( + algo: SizedAlgoKind, + tag: bool, + binary: bool, + raw: bool, + base64: bool, +) -> OutputFormat { + // Raw output format takes precedence over anything else. + if raw { + return OutputFormat::Raw; + } + + // Then, if the algo is legacy, takes precedence over the rest + if algo.is_legacy() { + return OutputFormat::Legacy; + } + + let digest_format = if base64 { + DigestFormat::Base64 + } else { + DigestFormat::Hexadecimal + }; + + // After that, decide between tagged and untagged output + if tag { + OutputFormat::Tagged(digest_format) + } else { + let reading_mode = if binary { + ReadingMode::Binary + } else { + ReadingMode::Text + }; + OutputFormat::Untagged(digest_format, reading_mode) + } +} + fn print_legacy_checksum( options: &ChecksumComputeOptions, filename: &OsStr, @@ -85,6 +141,14 @@ fn print_legacy_checksum( ) -> UResult<()> { debug_assert!(options.algo_kind.is_legacy()); + let (escaped_filename, prefix) = if options.line_ending == LineEnding::Nul { + (filename.to_string_lossy().to_string(), "") + } else { + escape_filename(filename) + }; + + print!("{prefix}"); + // Print the sum match options.algo_kind { SizedAlgoKind::Sysv => print!( @@ -108,9 +172,9 @@ fn print_legacy_checksum( } // Print the filename after a space if not stdin - if filename != "-" { + if escaped_filename != "-" { print!(" "); - let _dropped_result = io::stdout().write_all(os_str_as_bytes(filename)?); + let _dropped_result = io::stdout().write_all(escaped_filename.as_bytes()); } Ok(()) @@ -121,11 +185,17 @@ fn print_tagged_checksum( filename: &OsStr, sum: &String, ) -> UResult<()> { + let (escaped_filename, prefix) = if options.line_ending == LineEnding::Nul { + (filename.to_string_lossy().to_string(), "") + } else { + escape_filename(filename) + }; + // Print algo name and opening parenthesis. - print!("{} (", options.algo_kind.to_tag()); + print!("{prefix}{} (", options.algo_kind.to_tag()); // Print filename - let _dropped_result = io::stdout().write_all(os_str_as_bytes(filename)?); + let _dropped_result = io::stdout().write_all(escaped_filename.as_bytes()); // Print closing parenthesis and sum print!(") = {sum}"); @@ -134,15 +204,28 @@ fn print_tagged_checksum( } fn print_untagged_checksum( + options: &ChecksumComputeOptions, filename: &OsStr, sum: &String, reading_mode: ReadingMode, ) -> UResult<()> { + // early check for the "no-names" option + if options.no_names { + print!("{sum}"); + return Ok(()); + } + + let (escaped_filename, prefix) = if options.line_ending == LineEnding::Nul { + (filename.to_string_lossy().to_string(), "") + } else { + escape_filename(filename) + }; + // Print checksum and reading mode flag - print!("{sum} {}", reading_mode.as_char()); + print!("{prefix}{sum} {}", reading_mode.as_char()); // Print filename - let _dropped_result = io::stdout().write_all(os_str_as_bytes(filename)?); + let _dropped_result = io::stdout().write_all(escaped_filename.as_bytes()); Ok(()) } @@ -171,25 +254,30 @@ where if filepath.is_dir() { show!(USimpleError::new( 1, - translate!("cksum-error-is-directory", "file" => filepath.display()) + // TODO: Rework translation, which is broken since this code moved to uucore + // translate!("cksum-error-is-directory", "file" => filepath.display()) + format!("{}: Is a directory", filepath.display()) )); continue; } // Handle the file input - let mut file = BufReader::new(if filename == "-" { - stdin_buf = io::stdin(); - Box::new(stdin_buf) as Box - } else { - file_buf = match File::open(filepath) { - Ok(file) => file, - Err(err) => { - show!(err.map_err_context(|| filepath.to_string_lossy().to_string())); - continue; - } - }; - Box::new(file_buf) as Box - }); + let mut file = BufReader::with_capacity( + READ_BUFFER_SIZE, + if filename == "-" { + stdin_buf = io::stdin(); + Box::new(stdin_buf) as Box + } else { + file_buf = match File::open(filepath) { + Ok(file) => file, + Err(err) => { + show!(err.map_err_context(|| filepath.to_string_lossy().into())); + continue; + } + }; + Box::new(file_buf) as Box + }, + ); let mut digest = options.algo_kind.create_digest(); @@ -233,6 +321,7 @@ where } OutputFormat::Untagged(digest_format, reading_mode) => { print_untagged_checksum( + &options, filename, &encode_sum(sum_hex, digest_format), reading_mode, diff --git a/src/uucore/src/lib/features/checksum/mod.rs b/src/uucore/src/lib/features/checksum/mod.rs index a3b7e53c9..87c8836fd 100644 --- a/src/uucore/src/lib/features/checksum/mod.rs +++ b/src/uucore/src/lib/features/checksum/mod.rs @@ -2,25 +2,23 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore anotherfile invalidchecksum JWZG FFFD xffname prefixfilename bytelen bitlen hexdigit rsplit + +// spell-checker:ignore bitlen + +use std::ffi::OsStr; +use std::io::{self, Read}; +use std::num::IntErrorKind; use os_display::Quotable; -use std::{ - io::{self, Read}, - num::IntErrorKind, - path::Path, -}; - -use crate::{ - error::{UError, UResult}, - show_error, - sum::{ - Blake2b, Blake3, Bsd, CRC32B, Crc, Digest, DigestWriter, Md5, Sha1, Sha3_224, Sha3_256, - Sha3_384, Sha3_512, Sha224, Sha256, Sha384, Sha512, Shake128, Shake256, Sm3, SysV, - }, -}; use thiserror::Error; +use crate::error::{UError, UResult}; +use crate::show_error; +use crate::sum::{ + Blake2b, Blake3, Bsd, CRC32B, Crc, Digest, DigestWriter, Md5, Sha1, Sha3_224, Sha3_256, + Sha3_384, Sha3_512, Sha224, Sha256, Sha384, Sha512, Shake128, Shake256, Sm3, SysV, +}; + pub mod compute; pub mod validate; @@ -553,8 +551,8 @@ pub fn unescape_filename(filename: &[u8]) -> (Vec, &'static str) { (unescaped, prefix) } -pub fn escape_filename(filename: &Path) -> (String, &'static str) { - let original = filename.as_os_str().to_string_lossy(); +pub fn escape_filename(filename: &OsStr) -> (String, &'static str) { + let original = filename.to_string_lossy(); let escaped = original .replace('\\', "\\\\") .replace('\n', "\\n") @@ -587,19 +585,19 @@ mod tests { #[test] fn test_escape_filename() { - let (escaped, prefix) = escape_filename(Path::new("testfile.txt")); + let (escaped, prefix) = escape_filename(OsStr::new("testfile.txt")); assert_eq!(escaped, "testfile.txt"); assert_eq!(prefix, ""); - let (escaped, prefix) = escape_filename(Path::new("test\nfile.txt")); + let (escaped, prefix) = escape_filename(OsStr::new("test\nfile.txt")); assert_eq!(escaped, "test\\nfile.txt"); assert_eq!(prefix, "\\"); - let (escaped, prefix) = escape_filename(Path::new("test\rfile.txt")); + let (escaped, prefix) = escape_filename(OsStr::new("test\rfile.txt")); assert_eq!(escaped, "test\\rfile.txt"); assert_eq!(prefix, "\\"); - let (escaped, prefix) = escape_filename(Path::new("test\\file.txt")); + let (escaped, prefix) = escape_filename(OsStr::new("test\\file.txt")); assert_eq!(escaped, "test\\\\file.txt"); assert_eq!(prefix, "\\"); } From 6d6a9917ad08d7ceaf3eead765790fa0860d3635 Mon Sep 17 00:00:00 2001 From: Dorian Peron Date: Thu, 6 Nov 2025 20:18:39 +0100 Subject: [PATCH 078/182] checksum: fix binary flag on windows, ignore test --- src/uu/cksum/src/cksum.rs | 1 + src/uu/hashsum/src/hashsum.rs | 1 + src/uucore/src/lib/features/checksum/compute.rs | 5 ++++- src/uucore/src/lib/features/checksum/validate.rs | 12 ++++++++++-- tests/by-util/test_hashsum.rs | 13 ++++--------- 5 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/uu/cksum/src/cksum.rs b/src/uu/cksum/src/cksum.rs index 7ff0243eb..499fc52c0 100644 --- a/src/uu/cksum/src/cksum.rs +++ b/src/uu/cksum/src/cksum.rs @@ -185,6 +185,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { algo_kind: algo, output_format, line_ending, + binary: false, no_names: false, }; diff --git a/src/uu/hashsum/src/hashsum.rs b/src/uu/hashsum/src/hashsum.rs index ebe3ac033..d6258210f 100644 --- a/src/uu/hashsum/src/hashsum.rs +++ b/src/uu/hashsum/src/hashsum.rs @@ -229,6 +229,7 @@ pub fn uumain(mut args: impl uucore::Args) -> UResult<()> { /* base64: */ false, ), line_ending, + binary, no_names, }; diff --git a/src/uucore/src/lib/features/checksum/compute.rs b/src/uucore/src/lib/features/checksum/compute.rs index 426b2633e..e91c54166 100644 --- a/src/uucore/src/lib/features/checksum/compute.rs +++ b/src/uucore/src/lib/features/checksum/compute.rs @@ -29,6 +29,9 @@ pub struct ChecksumComputeOptions { /// Whether to finish lines with '\n' or '\0'. pub line_ending: LineEnding, + /// On windows, open files as binary instead of text + pub binary: bool, + /// (non-GNU option) Do not print file names pub no_names: bool, } @@ -284,7 +287,7 @@ where let (sum_hex, sz) = digest_reader( &mut digest, &mut file, - false, + options.binary, options.algo_kind.bitlen(), ) .map_err_context(|| translate!("cksum-error-failed-to-read-input"))?; diff --git a/src/uucore/src/lib/features/checksum/validate.rs b/src/uucore/src/lib/features/checksum/validate.rs index a1d851a05..b68032925 100644 --- a/src/uucore/src/lib/features/checksum/validate.rs +++ b/src/uucore/src/lib/features/checksum/validate.rs @@ -644,8 +644,16 @@ fn compute_and_check_digest_from_file( // Read the file and calculate the checksum let mut digest = algo.create_digest(); - let (calculated_checksum, _) = - digest_reader(&mut digest, &mut file_reader, false, algo.bitlen()).unwrap(); + + // TODO: improve function signature to use ReadingMode instead of binary bool + // Set binary to false because --binary is not supported with --check + let (calculated_checksum, _) = digest_reader( + &mut digest, + &mut file_reader, + /* binary */ false, + algo.bitlen(), + ) + .unwrap(); // Do the checksum validation let checksum_correct = expected_checksum == calculated_checksum; diff --git a/tests/by-util/test_hashsum.rs b/tests/by-util/test_hashsum.rs index b2eb96879..beaf994e1 100644 --- a/tests/by-util/test_hashsum.rs +++ b/tests/by-util/test_hashsum.rs @@ -107,17 +107,12 @@ macro_rules! test_digest { at.write("a", "file1\n"); at.write("c", "file3\n"); - #[cfg(unix)] - let file_not_found_str = "No such file or directory"; - #[cfg(not(unix))] - let file_not_found_str = "The system cannot find the file specified"; - ts.ucmd() .args(&[DIGEST_ARG, BITS_ARG, "a", "b", "c"]) .fails() .stdout_contains("a\n") .stdout_contains("c\n") - .stderr_contains(format!("b: {file_not_found_str}")); + .stderr_contains("b: No such file or directory"); } } )*) @@ -1097,11 +1092,11 @@ fn test_sha256_stdin_binary() { ); } +// This test is currently disabled on windows #[test] +#[cfg_attr(windows, ignore = "Discussion is in #9168")] fn test_check_sha256_binary() { - let ts = TestScenario::new(util_name!()); - - ts.ucmd() + new_ucmd!() .args(&[ "--sha256", "--bits=256", From 376fa6408b650cb778293ec9b641eb66aa327ce8 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Thu, 20 Nov 2025 00:39:51 +0900 Subject: [PATCH 079/182] GnuTests.yml: Check that build-gnu.sh works without libselinux (#9299) --- .github/workflows/GnuTests.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/GnuTests.yml b/.github/workflows/GnuTests.yml index 19d5e26ba..09752fe5f 100644 --- a/.github/workflows/GnuTests.yml +++ b/.github/workflows/GnuTests.yml @@ -80,7 +80,8 @@ jobs: run: | ## Install dependencies sudo apt-get update - sudo apt-get install -y autopoint gperf gdb python3-pyinotify valgrind libexpect-perl libacl1-dev libattr1-dev libcap-dev libselinux1-dev attr quilt + ## 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 - name: Add various locales shell: bash run: | From 15d22c285e672fa9dae6784c6f3d8becd9fb10d0 Mon Sep 17 00:00:00 2001 From: Vikram Kangotra <61800198+vikram-kangotra@users.noreply.github.com> Date: Wed, 19 Nov 2025 21:23:23 +0530 Subject: [PATCH 080/182] cp: allow directory merging when destination was just created (#9325) * cp: allow directory merging when destination was just created Previously, when copying to a destination that was created in the same cp call, the operation would fail with "will not overwrite just-created" for both files and directories. This change allows directories to be merged (matching GNU cp behavior) while still preventing file overwrites. The fix checks if both the source and destination are directories before allowing the merge. If either is a file, the original error behavior is preserved to prevent accidental file overwrites. Fixes the case where copying multiple directories to the same destination path would incorrectly error instead of merging their contents. fixes: #9318 * test_cp: Update with the suggestion Co-authored-by: Daniel Hofstetter --------- Co-authored-by: Daniel Hofstetter --- src/uu/cp/src/cp.rs | 17 +++++++++++++---- tests/by-util/test_cp.rs | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index c6c8789e2..9ef767d05 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -1375,10 +1375,19 @@ pub fn copy(sources: &[PathBuf], target: &Path, options: &Options) -> CopyResult { // There is already a file and it isn't a symlink (managed in a different place) if copied_destinations.contains(&dest) && options.backup != BackupMode::Numbered { - // If the target file was already created in this cp call, do not overwrite - return Err(CpError::Error( - translate!("cp-error-will-not-overwrite-just-created", "dest" => dest.quote(), "source" => source.quote()), - )); + // If the target was already created in this cp call, check if it's a directory. + // Directories should be merged (GNU cp behavior), but files should not be overwritten. + let dest_is_dir = fs::metadata(&dest).is_ok_and(|m| m.is_dir()); + let source_is_dir = fs::metadata(source).is_ok_and(|m| m.is_dir()); + + // Only prevent overwriting if both source and dest are files (not directories) + // Directories should be merged, which is handled by copy_directory + if !dest_is_dir || !source_is_dir { + // If the target file was already created in this cp call, do not overwrite + return Err(CpError::Error( + translate!("cp-error-will-not-overwrite-just-created", "dest" => dest.quote(), "source" => source.quote()), + )); + } } } diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index 3c5b3242e..c6f0d1c77 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -142,6 +142,41 @@ fn test_cp_duplicate_folder() { assert!(at.dir_exists(format!("{TEST_COPY_TO_FOLDER}/{TEST_COPY_FROM_FOLDER}").as_str())); } +#[test] +fn test_cp_duplicate_directories_merge() { + let (at, mut ucmd) = at_and_ucmd!(); + + // Source directory 1 + at.mkdir_all("src_dir/subdir"); + at.write("src_dir/subdir/file1.txt", "content1"); + at.write("src_dir/subdir/file2.txt", "content2"); + + // Source directory 2 + at.mkdir_all("src_dir2/subdir"); + at.write("src_dir2/subdir/file1.txt", "content3"); + + // Destination + at.mkdir("dest"); + + // Perform merge copy + ucmd.arg("-r") + .arg("src_dir/subdir") + .arg("src_dir2/subdir") + .arg("dest") + .succeeds(); + + // Verify directory exists + assert!(at.dir_exists("dest/subdir")); + + // file1.txt should be overwritten by src_dir2/subdir/file1.txt + assert!(at.file_exists("dest/subdir/file1.txt")); + assert_eq!(at.read("dest/subdir/file1.txt"), "content3"); + + // file2.txt should remain from first copy + assert!(at.file_exists("dest/subdir/file2.txt")); + assert_eq!(at.read("dest/subdir/file2.txt"), "content2"); +} + #[test] fn test_cp_duplicate_files_normalized_path() { let (at, mut ucmd) = at_and_ucmd!(); From 120d053cb1826d501c3a6b2a4a177d4f47a4f62f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 19 Nov 2025 21:01:11 +0000 Subject: [PATCH 081/182] chore(deps): update rust crate clap to v4.5.53 --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3c8aa5805..aa432a363 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -345,18 +345,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.52" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa8120877db0e5c011242f96806ce3c94e0737ab8108532a76a3300a01db2ab8" +checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.52" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02576b399397b659c26064fbc92a75fede9d18ffd5f80ca1cd74ddab167016e1" +checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" dependencies = [ "anstream", "anstyle", From 4b02a38be5ed4ee604a78a48fa9345320587dab3 Mon Sep 17 00:00:00 2001 From: FidelSch Date: Wed, 19 Nov 2025 20:36:26 -0300 Subject: [PATCH 082/182] ln: add error handling for hard link creation on directories --- src/uu/ln/locales/en-US.ftl | 1 + src/uu/ln/locales/fr-FR.ftl | 1 + src/uu/ln/src/ln.rs | 8 ++++++++ tests/by-util/test_ln.rs | 16 ++++++++++++++++ 4 files changed, 26 insertions(+) diff --git a/src/uu/ln/locales/en-US.ftl b/src/uu/ln/locales/en-US.ftl index 85315070d..54755c7dc 100644 --- a/src/uu/ln/locales/en-US.ftl +++ b/src/uu/ln/locales/en-US.ftl @@ -35,4 +35,5 @@ ln-prompt-replace = replace {$file}? ln-cannot-backup = cannot backup {$file} ln-failed-to-access = failed to access {$file} ln-failed-to-create-hard-link = failed to create hard link {$source} => {$dest} +ln-failed-to-create-hard-link-dir = {$source}: hard link not allowed for directory ln-backup = backup: {$backup} diff --git a/src/uu/ln/locales/fr-FR.ftl b/src/uu/ln/locales/fr-FR.ftl index 483f15c92..f037528c6 100644 --- a/src/uu/ln/locales/fr-FR.ftl +++ b/src/uu/ln/locales/fr-FR.ftl @@ -36,4 +36,5 @@ ln-prompt-replace = remplacer {$file} ? ln-cannot-backup = impossible de sauvegarder {$file} ln-failed-to-access = échec d'accès à {$file} ln-failed-to-create-hard-link = échec de création du lien physique {$source} => {$dest} +ln-failed-to-create-hard-link-dir = {$source} : lien physique non autorisé pour un répertoire ln-backup = sauvegarde : {$backup} diff --git a/src/uu/ln/src/ln.rs b/src/uu/ln/src/ln.rs index a3fde8f4a..e287dfa97 100644 --- a/src/uu/ln/src/ln.rs +++ b/src/uu/ln/src/ln.rs @@ -62,6 +62,9 @@ enum LnError { #[error("{}", translate!("ln-error-extra-operand", "operand" => _0.to_string_lossy(), "program" => _1.clone()))] ExtraOperand(OsString, String), + + #[error("{}", translate!("ln-failed-to-create-hard-link-dir", "source" => _0.to_string_lossy()))] + FailedToCreateHardLinkDir(PathBuf), } impl UError for LnError { @@ -431,6 +434,11 @@ fn link(src: &Path, dst: &Path, settings: &Settings) -> UResult<()> { if settings.symbolic { symlink(&source, dst)?; } else { + // Cannot create hard link to a directory + if src.is_dir() { + return Err(LnError::FailedToCreateHardLinkDir(source.to_path_buf()).into()); + } + let p = if settings.logical && source.is_symlink() { // if we want to have an hard link, // source is a symlink and -L is passed diff --git a/tests/by-util/test_ln.rs b/tests/by-util/test_ln.rs index d5a7bbfbb..bfcbc4e71 100644 --- a/tests/by-util/test_ln.rs +++ b/tests/by-util/test_ln.rs @@ -934,3 +934,19 @@ fn test_ln_non_utf8_paths() { let symlink_path = at.plus(symlink_name); assert!(symlink_path.is_symlink()); } + +#[test] +fn test_ln_hard_link_dir() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.mkdir("dir"); + + let result = scene.ucmd().args(&["dir", "dir_link"]).fails(); + + assert!( + result + .stderr_str() + .contains("hard link not allowed for directory") + ); +} From 366d5a3a20713cbf683067cd53813e0b8b807462 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 20 Nov 2025 00:47:52 +0000 Subject: [PATCH 083/182] chore(deps): update rust crate clap_complete to v4.5.61 --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3c8aa5805..a9a57f3ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -367,9 +367,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.60" +version = "4.5.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e602857739c5a4291dfa33b5a298aeac9006185229a700e5810a3ef7272d971" +checksum = "39615915e2ece2550c0149addac32fb5bd312c657f43845bb9088cb9c8a7c992" dependencies = [ "clap", ] From 43afd5cdaf74c0c56c4644609f5d11531d58d393 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Thu, 20 Nov 2025 15:00:42 +0900 Subject: [PATCH 084/182] GNUmakefile: Use .* for libstdbuf* matching --- GNUmakefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GNUmakefile b/GNUmakefile index b74cb6eeb..ceb48d2d1 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -450,7 +450,7 @@ install: build install-manpages install-completions install-locales mkdir -p $(INSTALLDIR_BIN) ifneq (,$(and $(findstring stdbuf,$(UTILS)),$(findstring feat_external_libstdbuf,$(CARGOFLAGS)))) mkdir -p $(DESTDIR)$(LIBSTDBUF_DIR) - $(INSTALL) -m 755 $(BUILDDIR)/deps/libstdbuf* $(DESTDIR)$(LIBSTDBUF_DIR)/ + $(INSTALL) -m 755 $(BUILDDIR)/deps/libstdbuf.* $(DESTDIR)$(LIBSTDBUF_DIR)/ endif ifeq (${MULTICALL}, y) $(INSTALL) -m 755 $(BUILDDIR)/coreutils $(INSTALLDIR_BIN)/$(PROG_PREFIX)coreutils @@ -473,7 +473,7 @@ endif uninstall: ifneq ($(OS),Windows_NT) - rm -f $(DESTDIR)$(LIBSTDBUF_DIR)/libstdbuf* + rm -f $(DESTDIR)$(LIBSTDBUF_DIR)/libstdbuf.* -rm -d $(DESTDIR)$(LIBSTDBUF_DIR) 2>/dev/null || true endif ifeq (${MULTICALL}, y) From 2a314c7ff397472e6ae38b618059ab95dd408833 Mon Sep 17 00:00:00 2001 From: Chris Dryden Date: Thu, 20 Nov 2025 03:36:56 -0500 Subject: [PATCH 085/182] fold: Adding combining character support (#9328) * Adding combining character support for fold * add fullwidth to the spell ignore list * addressing comments and cargo fmt fixes * clippy fixes for test files --------- Co-authored-by: Christopher Illarionova Co-authored-by: Sylvestre Ledru --- src/uu/fold/src/fold.rs | 9 +++++++++ tests/by-util/test_fold.rs | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/src/uu/fold/src/fold.rs b/src/uu/fold/src/fold.rs index f14ed3cf0..a2ddbed6a 100644 --- a/src/uu/fold/src/fold.rs +++ b/src/uu/fold/src/fold.rs @@ -434,6 +434,15 @@ fn process_utf8_line(line: &str, ctx: &mut FoldContext<'_, W>) -> URes let mut iter = line.char_indices().peekable(); while let Some((byte_idx, ch)) = iter.next() { + // Include combining characters with the base character + while let Some(&(_, next_ch)) = iter.peek() { + if unicode_width::UnicodeWidthChar::width(next_ch).unwrap_or(1) == 0 { + iter.next(); + } else { + break; + } + } + let next_idx = iter.peek().map(|(idx, _)| *idx).unwrap_or(line_bytes.len()); if ch == '\n' { diff --git a/tests/by-util/test_fold.rs b/tests/by-util/test_fold.rs index 04072ab15..9497044c9 100644 --- a/tests/by-util/test_fold.rs +++ b/tests/by-util/test_fold.rs @@ -2,6 +2,8 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +// spell-checker:ignore fullwidth + use uutests::new_ucmd; #[test] @@ -597,3 +599,36 @@ fn test_all_tab_advances_at_non_utf8_character() { .succeeds() .stdout_is_fixture_bytes("non_utf8_tab_stops_w16.expected"); } + +#[test] +fn test_combining_characters_nfc() { + // e acute NFC form (single character) + let e_acute_nfc = "\u{00E9}"; // é as single character + new_ucmd!() + .arg("-w2") + .pipe_in(format!("{e_acute_nfc}{e_acute_nfc}{e_acute_nfc}")) + .succeeds() + .stdout_is(format!("{e_acute_nfc}{e_acute_nfc}\n{e_acute_nfc}")); +} + +#[test] +fn test_combining_characters_nfd() { + // e acute NFD form (base + combining acute) + let e_acute_nfd = "e\u{0301}"; // e + combining acute accent + new_ucmd!() + .arg("-w2") + .pipe_in(format!("{e_acute_nfd}{e_acute_nfd}{e_acute_nfd}")) + .succeeds() + .stdout_is(format!("{e_acute_nfd}{e_acute_nfd}\n{e_acute_nfd}")); +} + +#[test] +fn test_fullwidth_characters() { + // e fullwidth (takes 2 columns) + let e_fullwidth = "\u{FF45}"; // e + new_ucmd!() + .arg("-w2") + .pipe_in(format!("{e_fullwidth}{e_fullwidth}")) + .succeeds() + .stdout_is(format!("{e_fullwidth}\n{e_fullwidth}")); +} From 0a5441fb0667e841ed6e6d18545c5d6c85966e2f Mon Sep 17 00:00:00 2001 From: FidelSch Date: Thu, 20 Nov 2025 09:12:39 -0300 Subject: [PATCH 086/182] ln: simplify hard link directory test assertion --- tests/by-util/test_ln.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/tests/by-util/test_ln.rs b/tests/by-util/test_ln.rs index bfcbc4e71..bc103a629 100644 --- a/tests/by-util/test_ln.rs +++ b/tests/by-util/test_ln.rs @@ -942,11 +942,9 @@ fn test_ln_hard_link_dir() { at.mkdir("dir"); - let result = scene.ucmd().args(&["dir", "dir_link"]).fails(); - - assert!( - result - .stderr_str() - .contains("hard link not allowed for directory") - ); + scene + .ucmd() + .args(&["dir", "dir_link"]) + .fails() + .stderr_contains("hard link not allowed for directory"); } From 2b8e67bee2ebf9285d6370b606e2a8b17008e52e Mon Sep 17 00:00:00 2001 From: FidelSch Date: Thu, 20 Nov 2025 10:56:55 -0300 Subject: [PATCH 087/182] ln: Allow hard links to soft links even when they poin to a directory --- src/uu/ln/src/ln.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/uu/ln/src/ln.rs b/src/uu/ln/src/ln.rs index e287dfa97..094106383 100644 --- a/src/uu/ln/src/ln.rs +++ b/src/uu/ln/src/ln.rs @@ -434,8 +434,9 @@ fn link(src: &Path, dst: &Path, settings: &Settings) -> UResult<()> { if settings.symbolic { symlink(&source, dst)?; } else { - // Cannot create hard link to a directory - if src.is_dir() { + // Cannot create hard link to a directory directly + // We can however create hard link to a symlink that points to a directory, so long as -L is not passed + if src.is_dir() && (!src.is_symlink() || settings.logical) { return Err(LnError::FailedToCreateHardLinkDir(source.to_path_buf()).into()); } From e1f2ba1c18468f9bfccf81a5ef11cb6bc33a7b72 Mon Sep 17 00:00:00 2001 From: FidelSch Date: Thu, 20 Nov 2025 12:36:50 -0300 Subject: [PATCH 088/182] ln: More helpful error message when trying to hard link to a directory --- util/build-gnu.sh | 3 --- 1 file changed, 3 deletions(-) diff --git a/util/build-gnu.sh b/util/build-gnu.sh index 3d2b509a2..f6874137d 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -268,9 +268,6 @@ sed -i -e "s/cat opts/sed -i -e \"s| <.\*$||g\" opts/" tests/misc/usage_vs_getop # for some reasons, some stuff are duplicated, strip that sed -i -e "s/provoked error./provoked error\ncat pat |sort -u > pat/" tests/misc/usage_vs_getopt.sh -# Update the GNU error message to match ours -sed -i -e "s/link-to-dir: hard link not allowed for directory/failed to create hard link 'link-to-dir' =>/" -e "s|link-to-dir/: hard link not allowed for directory|failed to create hard link 'link-to-dir/' =>|" tests/ln/hard-to-sym.sh - # install verbose messages shows ginstall as command sed -i -e "s/ginstall: creating directory/install: creating directory/g" tests/install/basic-1.sh From 44c1eeb68b00738ca5eea6cc9f28867eaaa5c8fe Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 20 Nov 2025 18:29:54 +0000 Subject: [PATCH 089/182] chore(deps): update actions/checkout action to v6 --- .github/workflows/CICD.yml | 36 ++++++++++++++--------------- .github/workflows/CheckScripts.yml | 4 ++-- .github/workflows/FixPR.yml | 2 +- .github/workflows/GnuTests.yml | 10 ++++---- .github/workflows/android.yml | 2 +- .github/workflows/benchmarks.yml | 2 +- .github/workflows/code-quality.yml | 12 +++++----- .github/workflows/devcontainer.yml | 2 +- .github/workflows/documentation.yml | 2 +- .github/workflows/freebsd.yml | 4 ++-- .github/workflows/fuzzing.yml | 8 +++---- .github/workflows/l10n.yml | 24 +++++++++---------- .github/workflows/openbsd.yml | 4 ++-- .github/workflows/wsl2.yml | 2 +- 14 files changed, 57 insertions(+), 57 deletions(-) diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index a1c88a316..ec812e7d5 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -36,7 +36,7 @@ jobs: name: Style/cargo-deny runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: EmbarkStudios/cargo-deny-action@v2 @@ -55,7 +55,7 @@ jobs: - { os: macos-latest , features: "feat_Tier1,feat_require_unix,feat_require_unix_utmpx" } - { os: windows-latest , features: feat_os_windows } steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@nightly @@ -109,7 +109,7 @@ jobs: # - { os: macos-latest , features: feat_os_macos } # - { os: windows-latest , features: feat_os_windows } steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@master @@ -168,7 +168,7 @@ jobs: job: - { os: ubuntu-latest , features: feat_os_unix } steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@master @@ -238,7 +238,7 @@ jobs: job: - { os: ubuntu-latest , features: feat_os_unix } steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@stable @@ -265,7 +265,7 @@ jobs: job: - { os: ubuntu-latest , features: feat_os_unix } steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@stable @@ -398,7 +398,7 @@ jobs: - { os: macos-latest , features: feat_os_macos } - { os: windows-latest , features: feat_os_windows } steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@stable @@ -427,7 +427,7 @@ jobs: - { os: macos-latest , features: feat_os_macos } - { os: windows-latest , features: feat_os_windows } steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@nightly @@ -453,7 +453,7 @@ jobs: job: - { os: ubuntu-latest , features: feat_os_unix } steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@stable @@ -593,7 +593,7 @@ jobs: - { os: windows-latest , target: x86_64-pc-windows-msvc , features: feat_os_windows } - { os: windows-latest , target: aarch64-pc-windows-msvc , features: feat_os_windows, use-cross: use-cross , skip-tests: true } steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@master @@ -875,7 +875,7 @@ jobs: run: | ## VARs setup echo "TEST_SUMMARY_FILE=busybox-result.json" >> $GITHUB_OUTPUT - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: Swatinem/rust-cache@v2 @@ -958,7 +958,7 @@ jobs: outputs() { step_id="${{ github.action }}"; for var in "$@" ; do echo steps.${step_id}.outputs.${var}="${!var}"; echo "${var}=${!var}" >> $GITHUB_OUTPUT; done; } TEST_SUMMARY_FILE="toybox-result.json" outputs TEST_SUMMARY_FILE - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@master @@ -1047,7 +1047,7 @@ jobs: # FIXME: Re-enable Code Coverage on windows, which currently fails due to "profiler_builtins". See #6686. # - { os: windows-latest , features: windows, toolchain: nightly-x86_64-pc-windows-gnu } steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: dtolnay/rust-toolchain@master with: toolchain: ${{ matrix.job.toolchain }} @@ -1164,7 +1164,7 @@ jobs: - { os: macos-latest , features: feat_os_macos } - { os: windows-latest , features: feat_os_windows } steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@stable @@ -1191,7 +1191,7 @@ jobs: - { os: macos-latest , features: feat_os_macos } # - { os: windows-latest , features: feat_os_windows } https://github.com/uutils/coreutils/issues/7044 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@stable @@ -1212,7 +1212,7 @@ jobs: needs: [ min_version, deps ] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@stable @@ -1254,7 +1254,7 @@ jobs: - { os: windows-latest , features: feat_os_windows } steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@stable @@ -1275,7 +1275,7 @@ jobs: needs: [ min_version, deps ] steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@stable diff --git a/.github/workflows/CheckScripts.yml b/.github/workflows/CheckScripts.yml index d58f75d83..218695366 100644 --- a/.github/workflows/CheckScripts.yml +++ b/.github/workflows/CheckScripts.yml @@ -29,7 +29,7 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - name: Run ShellCheck @@ -47,7 +47,7 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - name: Setup shfmt diff --git a/.github/workflows/FixPR.yml b/.github/workflows/FixPR.yml index e5de0584b..e8451c525 100644 --- a/.github/workflows/FixPR.yml +++ b/.github/workflows/FixPR.yml @@ -26,7 +26,7 @@ jobs: job: - { os: ubuntu-latest , features: feat_os_unix } steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - name: Initialize job variables diff --git a/.github/workflows/GnuTests.yml b/.github/workflows/GnuTests.yml index 09752fe5f..117d93c1c 100644 --- a/.github/workflows/GnuTests.yml +++ b/.github/workflows/GnuTests.yml @@ -37,7 +37,7 @@ jobs: steps: #### Get the code, setup cache - name: Checkout code (uutils) - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: path: 'uutils' persist-credentials: false @@ -59,7 +59,7 @@ jobs: with: workspaces: "./uutils -> target" - name: Checkout code (GNU coreutils) - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: repository: 'coreutils/coreutils' path: 'gnu' @@ -170,7 +170,7 @@ jobs: steps: #### Get the code, setup cache - name: Checkout code (uutils) - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: path: 'uutils' persist-credentials: false @@ -192,7 +192,7 @@ jobs: with: workspaces: "./uutils -> target" - name: Checkout code (GNU coreutils) - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: repository: 'coreutils/coreutils' path: 'gnu' @@ -329,7 +329,7 @@ jobs: outputs TEST_SUMMARY_FILE AGGREGATED_SUMMARY_FILE - name: Checkout code (uutils) - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: path: 'uutils' persist-credentials: false diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 46e2057eb..47a911973 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -80,7 +80,7 @@ jobs: echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules sudo udevadm control --reload-rules sudo udevadm trigger --name-match=kvm - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - name: Collect information about runner diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 37387a211..1c2245123 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -46,7 +46,7 @@ jobs: - { package: uu_wc } - { package: uu_factor } steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index aca6e829b..955a59eac 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -32,7 +32,7 @@ jobs: job: - { os: ubuntu-latest , features: feat_os_unix } steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@master @@ -82,7 +82,7 @@ jobs: - { os: macos-latest , features: feat_os_macos } - { os: windows-latest , features: feat_os_windows } steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@master @@ -156,7 +156,7 @@ jobs: job: - { os: ubuntu-latest , features: feat_os_unix } steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - name: Initialize workflow variables @@ -194,7 +194,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Clone repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: persist-credentials: false @@ -206,7 +206,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Clone repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: persist-credentials: false @@ -230,7 +230,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: persist-credentials: false diff --git a/.github/workflows/devcontainer.yml b/.github/workflows/devcontainer.yml index cbc0d0f87..ecc65bc99 100644 --- a/.github/workflows/devcontainer.yml +++ b/.github/workflows/devcontainer.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 45 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - name: Run test in devcontainer diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 47d5eb6ff..53104fb71 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Install/setup prerequisites shell: bash diff --git a/.github/workflows/freebsd.yml b/.github/workflows/freebsd.yml index 4a123789f..e78607c5b 100644 --- a/.github/workflows/freebsd.yml +++ b/.github/workflows/freebsd.yml @@ -34,7 +34,7 @@ jobs: SCCACHE_GHA_ENABLED: "true" RUSTC_WRAPPER: "sccache" steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: Swatinem/rust-cache@v2 @@ -130,7 +130,7 @@ jobs: SCCACHE_GHA_ENABLED: "true" RUSTC_WRAPPER: "sccache" steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: Swatinem/rust-cache@v2 diff --git a/.github/workflows/fuzzing.yml b/.github/workflows/fuzzing.yml index 42c247106..f7ba66595 100644 --- a/.github/workflows/fuzzing.yml +++ b/.github/workflows/fuzzing.yml @@ -21,7 +21,7 @@ jobs: name: Build and test uufuzz examples runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@stable @@ -55,7 +55,7 @@ jobs: name: Build the fuzzers runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@nightly @@ -99,7 +99,7 @@ jobs: - { name: fuzz_non_utf8_paths, should_pass: true } steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@nightly @@ -211,7 +211,7 @@ jobs: runs-on: ubuntu-latest if: always() steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - name: Download all stats diff --git a/.github/workflows/l10n.yml b/.github/workflows/l10n.yml index 1d244c6fb..3da0ba408 100644 --- a/.github/workflows/l10n.yml +++ b/.github/workflows/l10n.yml @@ -36,7 +36,7 @@ jobs: - { os: macos-latest , features: "feat_os_macos" } - { os: windows-latest , features: "feat_os_windows" } steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@stable @@ -76,7 +76,7 @@ jobs: name: L10n/Fluent Syntax Check runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - name: Setup Python @@ -131,7 +131,7 @@ jobs: SCCACHE_GHA_ENABLED: "true" RUSTC_WRAPPER: "sccache" steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@stable @@ -301,7 +301,7 @@ jobs: SCCACHE_GHA_ENABLED: "true" RUSTC_WRAPPER: "sccache" steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@stable @@ -416,7 +416,7 @@ jobs: - { os: ubuntu-latest , features: "feat_os_unix" } - { os: macos-latest , features: "feat_os_macos" } steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@stable @@ -567,7 +567,7 @@ jobs: - { os: ubuntu-latest , features: "feat_os_unix" } - { os: macos-latest , features: "feat_os_macos" } steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@stable @@ -900,7 +900,7 @@ jobs: SCCACHE_GHA_ENABLED: "true" RUSTC_WRAPPER: "sccache" steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@stable @@ -1131,7 +1131,7 @@ jobs: SCCACHE_GHA_ENABLED: "true" RUSTC_WRAPPER: "sccache" steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@stable @@ -1151,7 +1151,7 @@ jobs: name: L10n/Locale Embedding - Cat Utility runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@stable @@ -1183,7 +1183,7 @@ jobs: name: L10n/Locale Embedding - Ls Utility runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@stable @@ -1215,7 +1215,7 @@ jobs: name: L10n/Locale Embedding - Multicall Binary runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@stable @@ -1252,7 +1252,7 @@ jobs: SCCACHE_GHA_ENABLED: "true" RUSTC_WRAPPER: "sccache" steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@stable diff --git a/.github/workflows/openbsd.yml b/.github/workflows/openbsd.yml index 9ca20eab3..f7bae27ef 100644 --- a/.github/workflows/openbsd.yml +++ b/.github/workflows/openbsd.yml @@ -31,7 +31,7 @@ jobs: job: - { features: unix } steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - name: Prepare, build and test @@ -121,7 +121,7 @@ jobs: job: - { features: unix } steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - name: Prepare, build and test diff --git a/.github/workflows/wsl2.yml b/.github/workflows/wsl2.yml index 4f342847c..1764a03fc 100644 --- a/.github/workflows/wsl2.yml +++ b/.github/workflows/wsl2.yml @@ -27,7 +27,7 @@ jobs: job: - { os: windows-latest, distribution: Ubuntu-24.04, features: feat_os_unix} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - name: Install WSL2 From 7b47d581d544650e160671599567e5a0433511d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dorian=20P=C3=A9ron?= Date: Thu, 20 Nov 2025 01:39:14 +0100 Subject: [PATCH 090/182] checksum: fix GNU test cksum-base64-untagged.sh --- .../src/lib/features/checksum/validate.rs | 68 +++++++++++-------- 1 file changed, 39 insertions(+), 29 deletions(-) diff --git a/src/uucore/src/lib/features/checksum/validate.rs b/src/uucore/src/lib/features/checksum/validate.rs index b68032925..6b0595a42 100644 --- a/src/uucore/src/lib/features/checksum/validate.rs +++ b/src/uucore/src/lib/features/checksum/validate.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 rsplit hexdigit bitlen bytelen invalidchecksum xffname +// spell-checker:ignore rsplit hexdigit bitlen bytelen invalidchecksum inva idchecksum xffname use std::borrow::Cow; use std::ffi::OsStr; @@ -296,27 +296,7 @@ impl LineFormat { SubCase::OpenSSL => ByteSliceExt::rsplit_once(after_paren, b")= ")?, }; - fn is_valid_checksum(checksum: &[u8]) -> bool { - if checksum.is_empty() { - return false; - } - - let mut parts = checksum.splitn(2, |&b| b == b'='); - let main = parts.next().unwrap(); // Always exists since checksum isn't empty - let padding = parts.next().unwrap_or_default(); // Empty if no '=' - - main.iter() - .all(|&b| b.is_ascii_alphanumeric() || b == b'+' || b == b'/') - && !main.is_empty() - && padding.len() <= 2 - && padding.iter().all(|&b| b == b'=') - } - if !is_valid_checksum(checksum) { - return None; - } - // SAFETY: we just validated the contents of checksum, we can unsafely make a - // String from it - let checksum_utf8 = unsafe { String::from_utf8_unchecked(checksum.to_vec()) }; + let checksum_utf8 = Self::validate_checksum_format(checksum)?; Some(LineInfo { algo_name: Some(algo_utf8), @@ -336,12 +316,8 @@ impl LineFormat { fn parse_untagged(line: &[u8]) -> Option { let space_idx = line.iter().position(|&b| b == b' ')?; let checksum = &line[..space_idx]; - if !checksum.iter().all(|&b| b.is_ascii_hexdigit()) || checksum.is_empty() { - return None; - } - // SAFETY: we just validated the contents of checksum, we can unsafely make a - // String from it - let checksum_utf8 = unsafe { String::from_utf8_unchecked(checksum.to_vec()) }; + + let checksum_utf8 = Self::validate_checksum_format(checksum)?; let rest = &line[space_idx..]; let filename = rest @@ -388,6 +364,34 @@ impl LineFormat { format: Self::SingleSpace, }) } + + /// Ensure that the given checksum is syntactically valid (that it is either + /// hexadecimal or base64 encoded). + fn validate_checksum_format(checksum: &[u8]) -> Option { + if checksum.is_empty() { + return None; + } + + let mut parts = checksum.splitn(2, |&b| b == b'='); + let main = parts.next().unwrap(); // Always exists since checksum isn't empty + let padding = parts.next().unwrap_or_default(); // Empty if no '=' + + if main.is_empty() + || !main + .iter() + .all(|&b| b.is_ascii_alphanumeric() || b == b'+' || b == b'/') + { + return None; + } + + if padding.len() > 2 || padding.iter().any(|&b| b != b'=') { + return None; + } + + // SAFETY: we just validated the contents of checksum, we can unsafely make a + // String from it + Some(unsafe { String::from_utf8_unchecked(checksum.to_vec()) }) + } } // Helper trait for byte slice operations @@ -1039,7 +1043,13 @@ mod tests { b"b064a020db8018f18ff5ae367d01b212 ", Some((b"b064a020db8018f18ff5ae367d01b212", b" ")), ), - (b"invalidchecksum test", None), + // base64 checksums are accepted + ( + b"b21lbGV0dGUgZHUgZnJvbWFnZQ== ", + Some((b"b21lbGV0dGUgZHUgZnJvbWFnZQ==", b" ")), + ), + // Invalid checksums fail + (b"inva|idchecksum test", None), ]; for (input, expected) in test_cases { From b8cef3c2db140460f818b02d4df926be9864be53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dorian=20P=C3=A9ron?= Date: Thu, 20 Nov 2025 02:20:00 +0100 Subject: [PATCH 091/182] test(cksum): Implement the GNU cksum-base64-untagged.sh test --- tests/by-util/test_cksum.rs | 113 ++++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/tests/by-util/test_cksum.rs b/tests/by-util/test_cksum.rs index 4b9459ef5..57f11b8ef 100644 --- a/tests/by-util/test_cksum.rs +++ b/tests/by-util/test_cksum.rs @@ -2319,6 +2319,119 @@ mod gnu_cksum_base64 { } } +/// This module reimplements the cksum-base64-untagged.sh GNU test. +mod gnu_cksum_base64_untagged { + use super::*; + + macro_rules! decl_sha_test { + ($id:ident, $algo:literal, $len:expr) => { + mod $id { + use super::*; + + #[test] + fn check_length_guess() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + + at.write("inp", "test input\n"); + + let compute = ts + .ucmd() + .arg("-a") + .arg($algo) + .arg("-l") + .arg(stringify!($len)) + .arg("--base64") + .arg("--untagged") + .arg("inp") + .succeeds(); + + at.write_bytes("check", compute.stdout()); + + ts.ucmd() + .arg("-a") + .arg($algo) + .arg("--check") + .arg("check") + .succeeds() + .stdout_only("inp: OK\n"); + + at.write("check", " inp"); + + ts.ucmd() + .arg("-a") + .arg($algo) + .arg("check") + .fails() + .stderr_contains(concat!( + "--algorithm=", + $algo, + " requires specifying --length" + )); + } + } + }; + } + + decl_sha_test!(sha2_224, "sha2", 224); + decl_sha_test!(sha2_256, "sha2", 256); + decl_sha_test!(sha2_384, "sha2", 384); + decl_sha_test!(sha2_512, "sha2", 512); + decl_sha_test!(sha3_224, "sha3", 224); + decl_sha_test!(sha3_256, "sha3", 256); + decl_sha_test!(sha3_384, "sha3", 384); + decl_sha_test!(sha3_512, "sha3", 512); + + macro_rules! decl_blake_test { + ($id:ident, $len:expr) => { + mod $id { + use super::*; + + #[test] + fn check_length_guess() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + + at.write("inp", "test input\n"); + + let compute = ts + .ucmd() + .arg("-a") + .arg("blake2b") + .arg("-l") + .arg(stringify!($len)) + .arg("--base64") + .arg("--untagged") + .arg("inp") + .succeeds(); + + at.write_bytes("check", compute.stdout()); + + ts.ucmd() + .arg("-a") + .arg("blake2b") + .arg("--check") + .arg("check") + .succeeds() + .stdout_only("inp: OK\n"); + } + } + }; + } + + decl_blake_test!(blake2b_8, 8); + decl_blake_test!(blake2b_216, 216); + decl_blake_test!(blake2b_224, 224); + decl_blake_test!(blake2b_232, 232); + decl_blake_test!(blake2b_248, 248); + decl_blake_test!(blake2b_256, 256); + decl_blake_test!(blake2b_264, 264); + decl_blake_test!(blake2b_376, 376); + decl_blake_test!(blake2b_384, 384); + decl_blake_test!(blake2b_392, 392); + decl_blake_test!(blake2b_504, 504); + decl_blake_test!(blake2b_512, 512); +} /// This module reimplements the cksum-c.sh GNU test. mod gnu_cksum_c { use super::*; From e0c7ef7b10dac42ad24222104bc107a078a9763f Mon Sep 17 00:00:00 2001 From: naoNao89 <90588855+naoNao89@users.noreply.github.com> Date: Fri, 21 Nov 2025 08:02:05 +0700 Subject: [PATCH 092/182] fix(stdbuf): remove unsafe unwrap() calls for proper error handling - Replace get_many().unwrap() with ok_or_else() to handle missing command - Replace next().unwrap() with pattern matching for safety - Replace tempdir().unwrap() with map_err() to handle temp dir failures All stdbuf internal errors now properly exit with code 125 per GNU specification: https://www.gnu.org/s/coreutils/manual/html_node/stdbuf-invocation.html --- src/uu/stdbuf/src/stdbuf.rs | 38 ++++++++++++++++++++++++++++-------- tests/by-util/test_stdbuf.rs | 11 +++++++++++ 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/src/uu/stdbuf/src/stdbuf.rs b/src/uu/stdbuf/src/stdbuf.rs index 52824ad5e..fae2942f0 100644 --- a/src/uu/stdbuf/src/stdbuf.rs +++ b/src/uu/stdbuf/src/stdbuf.rs @@ -7,7 +7,6 @@ use clap::{Arg, ArgAction, ArgMatches, Command}; use std::ffi::OsString; -use std::os::unix::process::ExitStatusExt; use std::path::PathBuf; use std::process; use tempfile::TempDir; @@ -188,11 +187,17 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let options = ProgramOptions::try_from(&matches).map_err(|e| UUsageError::new(125, e.to_string()))?; - let mut command_values = matches.get_many::(options::COMMAND).unwrap(); - let mut command = process::Command::new(command_values.next().unwrap()); + let mut command_values = matches + .get_many::(options::COMMAND) + .ok_or_else(|| UUsageError::new(125, "no command specified".to_string()))?; + let Some(first_command) = command_values.next() else { + return Err(UUsageError::new(125, "no command specified".to_string())); + }; + let mut command = process::Command::new(first_command); let command_params: Vec<&OsString> = command_values.collect(); - let tmp_dir = tempdir().unwrap(); + let tmp_dir = tempdir() + .map_err(|e| UUsageError::new(125, format!("failed to create temp directory: {e}")))?; let (preload_env, libstdbuf) = get_preload_env(&tmp_dir)?; command.env(preload_env, libstdbuf); set_command_env(&mut command, "_STDBUF_I", &options.stdin); @@ -229,10 +234,27 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { Err(i.into()) } } - None => Err(USimpleError::new( - 1, - translate!("stdbuf-error-killed-by-signal", "signal" => status.signal().unwrap()), - )), + None => { + #[cfg(unix)] + { + use std::os::unix::process::ExitStatusExt; + let signal_msg = status + .signal() + .map(|s| s.to_string()) + .unwrap_or_else(|| "unknown".to_string()); + Err(USimpleError::new( + 1, + translate!("stdbuf-error-killed-by-signal", "signal" => signal_msg), + )) + } + #[cfg(not(unix))] + { + Err(USimpleError::new( + 1, + "process terminated abnormally".to_string(), + )) + } + } } } diff --git a/tests/by-util/test_stdbuf.rs b/tests/by-util/test_stdbuf.rs index 8c3fef587..a19900db3 100644 --- a/tests/by-util/test_stdbuf.rs +++ b/tests/by-util/test_stdbuf.rs @@ -87,6 +87,17 @@ fn test_stdbuf_no_buffer_option_fails() { .stderr_contains("the following required arguments were not provided:"); } +#[cfg(not(target_os = "windows"))] +#[test] +fn test_stdbuf_no_command_fails_with_125() { + // Test that missing command fails with exit code 125 (stdbuf error) + // This verifies proper error handling without unwrap panic + new_ucmd!() + .args(&["-o1"]) + .fails_with_code(125) + .stderr_contains("the following required arguments were not provided:"); +} + // Disabled on x86_64-unknown-linux-musl because the cross-rs Docker image for this target // does not provide musl-compiled system utilities (like tail), leading to dynamic linker errors // when preloading musl-compiled libstdbuf.so into glibc-compiled binaries. Same thing for FreeBSD. From 4d51ee3fa54af47a1bd3ddb7c09ee3fee3770463 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 21 Nov 2025 10:17:10 +0000 Subject: [PATCH 093/182] chore(deps): update rust crate parse_datetime to v0.13.3 --- Cargo.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fe230f9db..fa7a7d13f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1578,7 +1578,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde_core", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -1876,7 +1876,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]] @@ -2034,9 +2034,9 @@ dependencies = [ [[package]] name = "parse_datetime" -version = "0.13.2" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4955561bc7aa4c40afcfd2a8c34297b13164ae9ac3b30ac348737befdc98e4c" +checksum = "acea383beda9652270f3c9678d83aa58cbfc16880343cae0c0c8c7d6c0974132" dependencies = [ "jiff", "num-traits", @@ -2441,7 +2441,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.11.0", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -2747,7 +2747,7 @@ dependencies = [ "getrandom 0.3.3", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[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.61.2", + "windows-sys 0.59.0", ] [[package]] From 26a9fb7955a7955f18846c2b3f9e40b465266e84 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Fri, 21 Nov 2025 22:31:10 +0900 Subject: [PATCH 094/182] android.yml: Reduce RAM --- .github/workflows/android.yml | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 47a911973..0dac4e358 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -22,7 +22,7 @@ concurrency: env: TERMUX: v0.118.0 KEY_POSTFIX: nextest+rustc-hash+adb+sshd+upgrade+XGB+inc18 - COMMON_EMULATOR_OPTIONS: -no-window -noaudio -no-boot-anim -camera-back none -gpu swiftshader_indirect -metrics-collection + COMMON_EMULATOR_OPTIONS: -no-window -noaudio -no-boot-anim -camera-back none -gpu off EMULATOR_DISK_SIZE: 12GB EMULATOR_HEAP_SIZE: 2048M EMULATOR_BOOT_TIMEOUT: 1200 # 20min @@ -36,15 +36,10 @@ jobs: matrix: os: [ubuntu-latest] # , macos-latest cores: [4] # , 6 - ram: [4096, 8192] + ram: [4096] api-level: [28] target: [google_apis_playstore] arch: [x86, x86_64] # , arm64-v8a - exclude: - - ram: 8192 - arch: x86 - - ram: 4096 - arch: x86_64 runs-on: ${{ matrix.os }} env: EMULATOR_RAM_SIZE: ${{ matrix.ram }} From 29adc24d0e381e43886eb5c80c1d2727ca784e65 Mon Sep 17 00:00:00 2001 From: Chris Dryden Date: Fri, 21 Nov 2025 17:40:00 -0500 Subject: [PATCH 095/182] Adding TTY helper for unix to be able to create tests for stty and more (#9348) * Adding TTY helper for unix to be able to create tests for stty and more * removing missing flag on github actions and spellcheck ignore --- tests/by-util/test_stty.rs | 45 ++++++++++++++++++++++------------- tests/uutests/src/lib/util.rs | 19 +++++++++++++++ 2 files changed, 47 insertions(+), 17 deletions(-) diff --git a/tests/by-util/test_stty.rs b/tests/by-util/test_stty.rs index 8f4aec5bd..d6870d48f 100644 --- a/tests/by-util/test_stty.rs +++ b/tests/by-util/test_stty.rs @@ -2,9 +2,10 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore parenb parmrk ixany iuclc onlcr ofdel icanon noflsh econl igpar ispeed ospeed +// spell-checker:ignore parenb parmrk ixany iuclc onlcr icanon noflsh econl igpar ispeed ospeed use uutests::new_ucmd; +use uutests::util::pty_path; #[test] fn test_invalid_arg() { @@ -12,31 +13,41 @@ fn test_invalid_arg() { } #[test] -#[ignore = "Fails because cargo test does not run in a tty"] -fn runs() { - new_ucmd!().succeeds(); +#[cfg(unix)] +fn test_basic() { + let (path, _controller, _replica) = pty_path(); + new_ucmd!() + .args(&["--file", &path]) + .succeeds() + .stdout_contains("speed"); } #[test] -#[ignore = "Fails because cargo test does not run in a tty"] -fn print_all() { - let res = new_ucmd!().args(&["--all"]).succeeds(); +#[cfg(unix)] +fn test_all_flag() { + let (path, _controller, _replica) = pty_path(); + let result = new_ucmd!().args(&["--all", "--file", &path]).succeeds(); - // Random selection of flags to check for - for flag in [ - "parenb", "parmrk", "ixany", "onlcr", "ofdel", "icanon", "noflsh", - ] { - res.stdout_contains(flag); + for flag in ["parenb", "parmrk", "ixany", "onlcr", "icanon", "noflsh"] { + result.stdout_contains(flag); } } #[test] -#[ignore = "Fails because cargo test does not run in a tty"] -fn sane_settings() { - new_ucmd!().args(&["intr", "^A"]).succeeds(); - new_ucmd!().succeeds().stdout_contains("intr = ^A"); +#[cfg(unix)] +fn test_sane() { + let (path, _controller, _replica) = pty_path(); + new_ucmd!() - .args(&["sane"]) + .args(&["--file", &path, "intr", "^A"]) + .succeeds(); + new_ucmd!() + .args(&["--file", &path]) + .succeeds() + .stdout_contains("intr = ^A"); + new_ucmd!().args(&["--file", &path, "sane"]).succeeds(); + new_ucmd!() + .args(&["--file", &path]) .succeeds() .stdout_str_check(|s| !s.contains("intr = ^A")); } diff --git a/tests/uutests/src/lib/util.rs b/tests/uutests/src/lib/util.rs index ebd97ee5e..4668e7ba8 100644 --- a/tests/uutests/src/lib/util.rs +++ b/tests/uutests/src/lib/util.rs @@ -4,6 +4,7 @@ // file that was distributed with this source code. //spell-checker: ignore (linux) rlimit prlimit coreutil ggroups uchild uncaptured scmd SHLVL canonicalized openpty //spell-checker: ignore (linux) winsize xpixel ypixel setrlimit FSIZE SIGBUS SIGSEGV sigbus tmpfs mksocket +//spell-checker: ignore (ToDO) ttyname #![allow(dead_code)] #![allow( @@ -2886,6 +2887,24 @@ pub fn whoami() -> String { }) } +/// Create a PTY (pseudo-terminal) for testing utilities that require a TTY. +/// +/// Returns a tuple of (path, controller_fd, replica_fd) where: +/// - path: The filesystem path to the PTY replica device +/// - controller_fd: The controller file descriptor +/// - replica_fd: The replica file descriptor +#[cfg(unix)] +pub fn pty_path() -> (String, OwnedFd, OwnedFd) { + use nix::pty::openpty; + use nix::unistd::ttyname; + let pty = openpty(None, None).expect("Failed to create PTY"); + let path = ttyname(&pty.slave) + .expect("Failed to get PTY path") + .to_string_lossy() + .to_string(); + (path, pty.master, pty.slave) +} + /// Add prefix 'g' for `util_name` if not on linux #[cfg(unix)] pub fn host_name_for(util_name: &str) -> Cow<'_, str> { From 10bcf65afaaef5dafd7e1ca6050cc2418abb00fb Mon Sep 17 00:00:00 2001 From: Christopher Dryden Date: Fri, 21 Nov 2025 23:14:10 +0000 Subject: [PATCH 096/182] Using the pty helper function for the more bin testing --- tests/by-util/test_more.rs | 294 +++++++++++++++++++++++++------------ 1 file changed, 201 insertions(+), 93 deletions(-) diff --git a/tests/by-util/test_more.rs b/tests/by-util/test_more.rs index a46648a8b..cc8557dfd 100644 --- a/tests/by-util/test_more.rs +++ b/tests/by-util/test_more.rs @@ -3,144 +3,252 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use std::io::IsTerminal; - +#[cfg(unix)] +use nix::unistd::{read, write}; +#[cfg(unix)] +use std::fs::File; +#[cfg(unix)] +use std::fs::{Permissions, set_permissions}; +#[cfg(target_os = "linux")] +use std::os::unix::ffi::OsStrExt; +#[cfg(unix)] +use std::os::unix::fs::PermissionsExt; +#[cfg(unix)] +use uutests::util::pty_path; use uutests::{at_and_ucmd, new_ucmd}; +#[cfg(unix)] +fn run_more_with_pty( + args: &[&str], + file: &str, + content: &str, +) -> (uutests::util::UChild, std::os::fd::OwnedFd, String) { + let (path, controller, _replica) = pty_path(); + let (at, mut ucmd) = at_and_ucmd!(); + at.write(file, content); + + let mut child = ucmd + .set_stdin(File::open(&path).unwrap()) + .set_stdout(File::create(&path).unwrap()) + .args(args) + .arg(file) + .run_no_wait(); + + child.delay(100); + let mut output = vec![0u8; 1024]; + let n = read(&controller, &mut output).unwrap(); + let output_str = String::from_utf8_lossy(&output[..n]).to_string(); + + (child, controller, output_str) +} + +#[cfg(unix)] +fn quit_more(controller: &std::os::fd::OwnedFd, mut child: uutests::util::UChild) { + write(controller, b"q").unwrap(); + child.delay(50); +} + #[cfg(unix)] #[test] fn test_no_arg() { - if std::io::stdout().is_terminal() { - new_ucmd!() - .terminal_simulation(true) - .fails() - .stderr_contains("more: bad usage"); - } + let (path, _controller, _replica) = pty_path(); + new_ucmd!() + .set_stdin(File::open(&path).unwrap()) + .set_stdout(File::create(&path).unwrap()) + .fails() + .stderr_contains("more: bad usage"); } #[test] +#[cfg(unix)] fn test_valid_arg() { - if std::io::stdout().is_terminal() { - let args_list: Vec<&[&str]> = vec![ - &["-c"], - &["--clean-print"], - &["-p"], - &["--print-over"], - &["-s"], - &["--squeeze"], - &["-u"], - &["--plain"], - &["-n", "10"], - &["--lines", "0"], - &["--number", "0"], - &["-F", "10"], - &["--from-line", "0"], - &["-P", "something"], - &["--pattern", "-1"], - ]; - for args in args_list { - test_alive(args); - } + let args_list: Vec<&[&str]> = vec![ + &["-c"], + &["--clean-print"], + &["-p"], + &["--print-over"], + &["-s"], + &["--squeeze"], + &["-u"], + &["--plain"], + &["-n", "10"], + &["--lines", "0"], + &["--number", "0"], + &["-F", "10"], + &["--from-line", "0"], + &["-P", "something"], + &["--pattern", "-1"], + ]; + for args in args_list { + test_alive(args); } } +#[cfg(unix)] fn test_alive(args: &[&str]) { let (at, mut ucmd) = at_and_ucmd!(); + let (path, controller, _replica) = pty_path(); let content = "test content"; let file = "test_file"; at.write(file, content); - let mut cmd = ucmd.args(args).arg(file).run_no_wait(); + let mut child = ucmd + .set_stdin(File::open(&path).unwrap()) + .set_stdout(File::create(&path).unwrap()) + .args(args) + .arg(file) + .run_no_wait(); // wait for more to start and display the file - while cmd.is_alive() && !cmd.stdout_all().contains(content) { - cmd.delay(50); - } + child.delay(100); - assert!(cmd.is_alive(), "Command should still be alive"); + assert!(child.is_alive(), "Command should still be alive"); // cleanup - cmd.kill(); + write(&controller, b"q").unwrap(); + child.delay(50); } #[test] +#[cfg(unix)] fn test_invalid_arg() { - if std::io::stdout().is_terminal() { - new_ucmd!().arg("--invalid").fails(); + let (path, _controller, _replica) = pty_path(); + new_ucmd!() + .set_stdin(File::open(&path).unwrap()) + .set_stdout(File::create(&path).unwrap()) + .arg("--invalid") + .fails(); - new_ucmd!().arg("--lines").arg("-10").fails(); - new_ucmd!().arg("--number").arg("-10").fails(); + let (path, _controller, _replica) = pty_path(); + new_ucmd!() + .set_stdin(File::open(&path).unwrap()) + .set_stdout(File::create(&path).unwrap()) + .arg("--lines") + .arg("-10") + .fails(); - new_ucmd!().arg("--from-line").arg("-10").fails(); - } + let (path, _controller, _replica) = pty_path(); + new_ucmd!() + .set_stdin(File::open(&path).unwrap()) + .set_stdout(File::create(&path).unwrap()) + .arg("--from-line") + .arg("-10") + .fails(); } #[test] +#[cfg(unix)] fn test_file_arg() { - // Run the test only if there's a valid terminal, else do nothing - // Maybe we could capture the error, i.e. "Device not found" in that case - // but I am leaving this for later - if std::io::stdout().is_terminal() { - // Directory as argument - new_ucmd!() - .arg(".") - .succeeds() - .stderr_contains("'.' is a directory."); + // Directory as argument + let (path, _controller, _replica) = pty_path(); + new_ucmd!() + .set_stdin(File::open(&path).unwrap()) + .set_stdout(File::create(&path).unwrap()) + .arg(".") + .succeeds() + .stderr_contains("'.' is a directory."); - // Single argument errors - let (at, mut ucmd) = at_and_ucmd!(); - at.mkdir_all("folder"); - ucmd.arg("folder") - .succeeds() - .stderr_contains("is a directory"); + // Single argument errors + let (path, _controller, _replica) = pty_path(); + let (at, mut ucmd) = at_and_ucmd!(); + at.mkdir_all("folder"); + ucmd.set_stdin(File::open(&path).unwrap()) + .set_stdout(File::create(&path).unwrap()) + .arg("folder") + .succeeds() + .stderr_contains("is a directory"); - new_ucmd!() - .arg("nonexistent_file") - .succeeds() - .stderr_contains("No such file or directory"); + let (path, _controller, _replica) = pty_path(); + new_ucmd!() + .set_stdin(File::open(&path).unwrap()) + .set_stdout(File::create(&path).unwrap()) + .arg("nonexistent_file") + .succeeds() + .stderr_contains("No such file or directory"); - // Multiple nonexistent files - new_ucmd!() - .arg("file2") - .arg("file3") - .succeeds() - .stderr_contains("file2") - .stderr_contains("file3"); - } + // Multiple nonexistent files + let (path, _controller, _replica) = pty_path(); + new_ucmd!() + .set_stdin(File::open(&path).unwrap()) + .set_stdout(File::create(&path).unwrap()) + .arg("file2") + .arg("file3") + .succeeds() + .stderr_contains("file2") + .stderr_contains("file3"); } #[test] -#[cfg(target_family = "unix")] +#[cfg(unix)] fn test_invalid_file_perms() { - if std::io::stdout().is_terminal() { - use std::fs::{Permissions, set_permissions}; - use std::os::unix::fs::PermissionsExt; - - let (at, mut ucmd) = at_and_ucmd!(); - let permissions = Permissions::from_mode(0o244); - at.make_file("invalid-perms.txt"); - set_permissions(at.plus("invalid-perms.txt"), permissions).unwrap(); - ucmd.arg("invalid-perms.txt") - .succeeds() - .stderr_contains("permission denied"); - } + let (path, _controller, _replica) = pty_path(); + let (at, mut ucmd) = at_and_ucmd!(); + let permissions = Permissions::from_mode(0o244); + at.make_file("invalid-perms.txt"); + set_permissions(at.plus("invalid-perms.txt"), permissions).unwrap(); + ucmd.set_stdin(File::open(&path).unwrap()) + .set_stdout(File::create(&path).unwrap()) + .arg("invalid-perms.txt") + .succeeds() + .stderr_contains("permission denied"); } #[test] #[cfg(target_os = "linux")] fn test_more_non_utf8_paths() { - use std::os::unix::ffi::OsStrExt; - if std::io::stdout().is_terminal() { - let (at, mut ucmd) = at_and_ucmd!(); - let file_name = std::ffi::OsStr::from_bytes(b"test_\xFF\xFE.txt"); - // Create test file with normal name first - at.write( - &file_name.to_string_lossy(), - "test content for non-UTF-8 file", - ); + let (path, _controller, _replica) = pty_path(); + let (at, mut ucmd) = at_and_ucmd!(); + let file_name = std::ffi::OsStr::from_bytes(b"test_\xFF\xFE.txt"); + // Create test file with normal name first + at.write( + &file_name.to_string_lossy(), + "test content for non-UTF-8 file", + ); - // Test that more can handle non-UTF-8 filenames without crashing - ucmd.arg(file_name).succeeds(); - } + // Test that more can handle non-UTF-8 filenames without crashing + ucmd.set_stdin(File::open(&path).unwrap()) + .set_stdout(File::create(&path).unwrap()) + .arg(file_name) + .succeeds(); +} + +#[test] +#[cfg(unix)] +fn test_basic_display() { + let (child, controller, output) = run_more_with_pty(&[], "test.txt", "line1\nline2\nline3\n"); + assert!(output.contains("line1")); + quit_more(&controller, child); +} + +#[test] +#[cfg(unix)] +fn test_squeeze_blank_lines() { + let (child, controller, output) = + run_more_with_pty(&["-s"], "test.txt", "line1\n\n\n\nline2\n"); + assert!(output.contains("line1")); + quit_more(&controller, child); +} + +#[test] +#[cfg(unix)] +fn test_pattern_search() { + let (child, controller, output) = run_more_with_pty( + &["-P", "target"], + "test.txt", + "foo\nbar\nbaz\ntarget\nend\n", + ); + assert!(output.contains("target")); + assert!(!output.contains("foo")); + quit_more(&controller, child); +} + +#[test] +#[cfg(unix)] +fn test_from_line_option() { + let (child, controller, output) = + run_more_with_pty(&["-F", "2"], "test.txt", "line1\nline2\nline3\nline4\n"); + assert!(output.contains("line2")); + assert!(!output.contains("line1")); + quit_more(&controller, child); } From a79c4bc6b48912cdbb878e4c12d394987edfa98e Mon Sep 17 00:00:00 2001 From: Christopher Dryden Date: Fri, 21 Nov 2025 23:39:21 +0000 Subject: [PATCH 097/182] Only using imports on unix since more integration tests only supported on unix --- tests/by-util/test_more.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/by-util/test_more.rs b/tests/by-util/test_more.rs index cc8557dfd..2bf130a18 100644 --- a/tests/by-util/test_more.rs +++ b/tests/by-util/test_more.rs @@ -15,6 +15,7 @@ use std::os::unix::ffi::OsStrExt; use std::os::unix::fs::PermissionsExt; #[cfg(unix)] use uutests::util::pty_path; +#[cfg(unix)] use uutests::{at_and_ucmd, new_ucmd}; #[cfg(unix)] From 8d7777b7babc371e5d0cd2a991a36272edec8275 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Sat, 22 Nov 2025 14:49:13 +0900 Subject: [PATCH 098/182] README.md: note that separator is needed for PROG_PREFIX --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 64785e8df..3ed607d42 100644 --- a/README.md +++ b/README.md @@ -228,9 +228,11 @@ make UTILS='UTILITY_1 UTILITY_2' install To install every program with a prefix (e.g. uu-echo uu-cat): ```shell -make PROG_PREFIX=PREFIX_GOES_HERE install +make PROG_PREFIX=uu- install ``` +`PROG_PREFIX` requires separator `-`, `_`, or `=`. + To install the multicall binary: ```shell @@ -320,7 +322,7 @@ make uninstall To uninstall every program with a set prefix: ```shell -make PROG_PREFIX=PREFIX_GOES_HERE uninstall +make PROG_PREFIX=uu- uninstall ``` To uninstall the multicall binary: From 60958b295c83ee85ffa025c58f49030c4edc7b0b Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Sun, 23 Nov 2025 02:32:58 +0900 Subject: [PATCH 099/182] installation.md: Fix ref for AUR --- docs/src/installation.md | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/docs/src/installation.md b/docs/src/installation.md index 856ca9d22..7b5805fc1 100644 --- a/docs/src/installation.md +++ b/docs/src/installation.md @@ -122,6 +122,12 @@ apt install rust-coreutils export PATH=/usr/lib/cargo/bin/coreutils:$PATH ``` +### AUR + +[AUR package](https://aur.archlinux.org/packages/uutils-coreutils-git) + +Rust rewrite of the GNU coreutils (main branch). + ## MacOS ### Homebrew @@ -184,11 +190,3 @@ Clone [poky](https://github.com/yoctoproject/poky) and [meta-openembedded](https and then either call `bitbake uutils-coreutils`, or use `PREFERRED_PROVIDER_coreutils = "uutils-coreutils"` in your `build/conf/local.conf` file and then build your usual yocto image. - -## Non-standard packages - -### `coreutils-uutils` (AUR) - -[AUR package](https://aur.archlinux.org/packages/coreutils-uutils) - -Cross-platform Rust rewrite of the GNU coreutils being used as actual system coreutils. From 6d48b9879e05e23f63883c23ab374ca0deb630d2 Mon Sep 17 00:00:00 2001 From: Chris Dryden Date: Sat, 22 Nov 2025 14:14:12 -0500 Subject: [PATCH 100/182] Removing the per process file flag to reduce the llvm filemerge time --- util/build-run-test-coverage-linux.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util/build-run-test-coverage-linux.sh b/util/build-run-test-coverage-linux.sh index 3eec0dda3..5a5b5af2a 100755 --- a/util/build-run-test-coverage-linux.sh +++ b/util/build-run-test-coverage-linux.sh @@ -57,7 +57,7 @@ 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-gnu" -export LLVM_PROFILE_FILE="${PROFRAW_DIR}/coverage-%m-%p.profraw" +export LLVM_PROFILE_FILE="${PROFRAW_DIR}/coverage-%4m.profraw" # Disable expanded command printing for the rest of the program set +x From e094bddcc97f48ad106d9821d5f779967a2f5178 Mon Sep 17 00:00:00 2001 From: Chris Dryden Date: Sat, 22 Nov 2025 13:41:39 -0500 Subject: [PATCH 101/182] Reducing sleep times and timeout times in test_timeout --- tests/by-util/test_timeout.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/by-util/test_timeout.rs b/tests/by-util/test_timeout.rs index b04b32203..27800f06d 100644 --- a/tests/by-util/test_timeout.rs +++ b/tests/by-util/test_timeout.rs @@ -55,11 +55,11 @@ fn test_command_with_args() { fn test_verbose() { for verbose_flag in ["-v", "--verbose"] { new_ucmd!() - .args(&[verbose_flag, ".1", "sleep", "10"]) + .args(&[verbose_flag, ".1", "sleep", "1"]) .fails() .stderr_only("timeout: sending signal TERM to command 'sleep'\n"); new_ucmd!() - .args(&[verbose_flag, "-s0", "-k.1", ".1", "sleep", "10"]) + .args(&[verbose_flag, "-s0", "-k.1", ".1", "sleep", "1"]) .fails() .stderr_only("timeout: sending signal EXIT to command 'sleep'\ntimeout: sending signal KILL to command 'sleep'\n"); } @@ -112,7 +112,7 @@ fn test_preserve_status_even_when_send_signal() { // So, expected result is success and code 0. for cont_spelling in ["CONT", "cOnT", "SIGcont"] { new_ucmd!() - .args(&["-s", cont_spelling, "--preserve-status", ".1", "sleep", "2"]) + .args(&["-s", cont_spelling, "--preserve-status", ".1", "sleep", "1"]) .succeeds() .no_output(); } @@ -186,10 +186,10 @@ fn test_kill_subprocess() { new_ucmd!() .args(&[ // Make sure the CI can spawn the subprocess. - "10", + "1", "sh", "-c", - "trap 'echo inside_trap' TERM; sleep 30", + "trap 'echo inside_trap' TERM; sleep 5", ]) .fails_with_code(124) .stdout_contains("inside_trap"); From 403c39ca635a172c1e5f68bc172b90436991d264 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Sun, 23 Nov 2025 17:37:36 +0900 Subject: [PATCH 102/182] installation.md: Ref MSYS2 package --- docs/src/installation.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/src/installation.md b/docs/src/installation.md index 7b5805fc1..537504cc5 100644 --- a/docs/src/installation.md +++ b/docs/src/installation.md @@ -1,4 +1,4 @@ - + # Installation @@ -170,6 +170,10 @@ winget install uutils.coreutils scoop install uutils-coreutils ``` +### MSYS2 + +[MSYS2 package](https://packages.msys2.org/base/mingw-w64-uutils-coreutils) + ## Alternative installers ### Conda From b568a653688a1615d09257fa449cda5152ed62f8 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Sun, 23 Nov 2025 18:02:23 +0900 Subject: [PATCH 103/182] build-gnu.sh: Drop a workaround for closed ssue --- util/build-gnu.sh | 2 -- 1 file changed, 2 deletions(-) diff --git a/util/build-gnu.sh b/util/build-gnu.sh index 4abcf8f48..ebfa74aa0 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -169,8 +169,6 @@ grep -rl '\$abs_path_dir_' tests/*/*.sh | xargs -r sed -i "s|\$abs_path_dir_|${U # Use the system coreutils where the test fails due to error in a util that is not the one being tested sed -i "s|grep '^#define HAVE_CAP 1' \$CONFIG_HEADER > /dev/null|true|" tests/ls/capability.sh -# tests/ls/abmon-align.sh - https://github.com/uutils/coreutils/issues/3505 -sed -i 's|touch |/usr/bin/touch |' tests/test/test-N.sh tests/ls/abmon-align.sh # our messages are better sed -i "s|cannot stat 'symlink': Permission denied|not writing through dangling symlink 'symlink'|" tests/cp/fail-perm.sh From fdcceddfa92c61766a6434da510af31ddb33cd7b Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Mon, 24 Nov 2025 00:05:13 +0900 Subject: [PATCH 104/182] build-gnu.sh: Remove which for portability (#9452) --- util/build-gnu.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/util/build-gnu.sh b/util/build-gnu.sh index ebfa74aa0..9fbc445b9 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -348,8 +348,8 @@ sed -i 's/not supported/unexpected argument/' tests/mv/mv-exchange.sh # Most tests check that `/usr/bin/tr` is working correctly before running. # However in NixOS/Nix-based distros, the tr util is located somewhere in # /nix/store/xxxxxxxxxxxx...xxxx/bin/tr -# We just replace the references to `/usr/bin/tr` with the result of `$(which tr)` -sed -i 's/\/usr\/bin\/tr/$(which tr)/' tests/init.sh +# We just replace the references to `/usr/bin/tr` +sed -i 's/\/usr\/bin\/tr/$(command -v tr)/' tests/init.sh # upstream doesn't having the program name in the error message # but we do. We should keep it that way. From b4423b96910c2dba0baab70a9b2552b3254ba601 Mon Sep 17 00:00:00 2001 From: Chris Dryden Date: Sun, 23 Nov 2025 15:21:53 -0500 Subject: [PATCH 105/182] Adding integration tests for the braced variable parsing in env (#9459) * Adding integration tests for the braced variable parsing in env * Adding missing spell checker words --- tests/by-util/test_env.rs | 63 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/tests/by-util/test_env.rs b/tests/by-util/test_env.rs index db8e0e793..68e7e03b5 100644 --- a/tests/by-util/test_env.rs +++ b/tests/by-util/test_env.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 (words) bamf chdir rlimit prlimit COMSPEC cout cerr FFFD winsize xpixel ypixel +// spell-checker:ignore (words) bamf chdir rlimit prlimit COMSPEC cout cerr FFFD winsize xpixel ypixel Secho #![allow(clippy::missing_errors_doc)] #[cfg(unix)] @@ -1801,3 +1801,64 @@ fn test_shebang_error() { .fails() .stderr_contains("use -[v]S to pass options in shebang lines"); } + +#[test] +#[cfg(not(target_os = "windows"))] +fn test_braced_variable_with_default_value() { + new_ucmd!() + .arg("-Secho ${UNSET_VAR_UNLIKELY_12345:fallback}") + .succeeds() + .stdout_is("fallback\n"); +} + +#[test] +#[cfg(not(target_os = "windows"))] +fn test_braced_variable_with_default_when_set() { + new_ucmd!() + .env("TEST_VAR_12345", "actual") + .arg("-Secho ${TEST_VAR_12345:fallback}") + .succeeds() + .stdout_is("actual\n"); +} + +#[test] +#[cfg(not(target_os = "windows"))] +fn test_simple_braced_variable() { + new_ucmd!() + .env("TEST_VAR_12345", "value") + .arg("-Secho ${TEST_VAR_12345}") + .succeeds() + .stdout_is("value\n"); +} + +#[test] +fn test_braced_variable_error_missing_closing_brace() { + new_ucmd!() + .arg("-Secho ${FOO") + .fails_with_code(125) + .stderr_contains("Missing closing brace"); +} + +#[test] +fn test_braced_variable_error_missing_closing_brace_after_default() { + new_ucmd!() + .arg("-Secho ${FOO:-value") + .fails_with_code(125) + .stderr_contains("Missing closing brace after default value"); +} + +#[test] +fn test_braced_variable_error_starts_with_digit() { + new_ucmd!() + .arg("-Secho ${1FOO}") + .fails_with_code(125) + .stderr_contains("Unexpected character: '1'"); +} + +#[test] +fn test_braced_variable_error_unexpected_character() { + new_ucmd!() + .arg("-Secho ${FOO?}") + .fails_with_code(125) + .stderr_contains("Unexpected character: '?'"); +} From bf24cb88803a83db75ab568d762e2bb1867346bc Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Mon, 24 Nov 2025 11:16:00 +0900 Subject: [PATCH 106/182] Do not apt-get preinstalled tools to avoid delaying --- .github/workflows/CICD.yml | 2 +- .github/workflows/code-quality.yml | 2 +- .github/workflows/l10n.yml | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index ec812e7d5..93f1fac79 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -465,7 +465,7 @@ jobs: run: | ## Install dependencies sudo apt-get update - sudo apt-get install jq libselinux1-dev libsystemd-dev + sudo apt-get install libselinux1-dev libsystemd-dev - name: "`make install`" shell: bash run: | diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 955a59eac..971c42bf4 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -174,7 +174,7 @@ jobs: - name: Install/setup prerequisites shell: bash run: | - sudo apt-get -y update ; sudo apt-get -y install npm ; sudo npm install cspell -g ; + sudo npm install cspell -g ; - name: Run `cspell` shell: bash run: | diff --git a/.github/workflows/l10n.yml b/.github/workflows/l10n.yml index 3da0ba408..9d6821738 100644 --- a/.github/workflows/l10n.yml +++ b/.github/workflows/l10n.yml @@ -141,7 +141,7 @@ jobs: - name: Install/setup prerequisites shell: bash run: | - sudo apt-get -y update ; sudo apt-get -y install libselinux1-dev locales + sudo apt-get -y update ; sudo apt-get -y install libselinux1-dev sudo locale-gen --keep-existing fr_FR.UTF-8 locale -a | grep -i fr || exit 1 - name: Build coreutils with clap localization support @@ -312,7 +312,7 @@ jobs: shell: bash run: | ## Install/setup prerequisites - sudo apt-get -y update ; sudo apt-get -y install libselinux1-dev locales + sudo apt-get -y update ; sudo apt-get -y install libselinux1-dev - name: Generate French locale shell: bash run: | @@ -580,7 +580,7 @@ jobs: ## Install/setup prerequisites case '${{ matrix.job.os }}' in ubuntu-*) - sudo apt-get -y update ; sudo apt-get -y install libselinux1-dev build-essential locales + sudo apt-get -y update ; sudo apt-get -y install libselinux1-dev build-essential # Generate French locale for testing sudo locale-gen --keep-existing fr_FR.UTF-8 locale -a | grep -i fr || echo "French locale generation may have failed" @@ -912,7 +912,7 @@ jobs: run: | ## Install/setup prerequisites including locale support sudo apt-get -y update - sudo apt-get -y install libselinux1-dev locales build-essential + sudo apt-get -y install libselinux1-dev build-essential # Generate multiple locales for testing sudo locale-gen --keep-existing en_US.UTF-8 fr_FR.UTF-8 de_DE.UTF-8 es_ES.UTF-8 @@ -1264,7 +1264,7 @@ jobs: - name: Install prerequisites run: | sudo apt-get -y update - sudo apt-get -y install libselinux1-dev locales + sudo apt-get -y install libselinux1-dev # Generate French locale for testing sudo locale-gen --keep-existing fr_FR.UTF-8 locale -a | grep -i fr || exit 1 From 1539cd2fe723a63ebf7a01ea19a1b855bcef4fed Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Mon, 24 Nov 2025 12:01:19 +0900 Subject: [PATCH 107/182] build-gnu.sh: use GNU sed much more for macOS --- util/build-gnu.sh | 123 ++++++++++++++++++++++------------------------ 1 file changed, 60 insertions(+), 63 deletions(-) diff --git a/util/build-gnu.sh b/util/build-gnu.sh index 9fbc445b9..46a2852ef 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -8,7 +8,7 @@ set -e -# Use system's GNU version for make, nproc, readlink and sed on *BSD +# Use system's GNU version for make, nproc, readlink and sed on *BSD and macOS MAKE=$(command -v gmake||command -v make) NPROC=$(command -v gnproc||command -v nproc) READLINK=$(command -v greadlink||command -v readlink) @@ -121,7 +121,7 @@ done # Always update the PATH to test the uutils coreutils instead of the GNU coreutils # This ensures the correct path is used even if the repository was moved or rebuilt in a different location -sed -i "s/^[[:blank:]]*PATH=.*/ PATH='${UU_BUILD_DIR//\//\\/}\$(PATH_SEPARATOR)'\"\$\$PATH\" \\\/" tests/local.mk +"${SED}" -i "s/^[[:blank:]]*PATH=.*/ PATH='${UU_BUILD_DIR//\//\\/}\$(PATH_SEPARATOR)'\"\$\$PATH\" \\\/" tests/local.mk if test -f gnu-built; then echo "GNU build already found. Skip" @@ -129,15 +129,15 @@ if test -f gnu-built; then echo "Note: the customization of the tests will still happen" else # Disable useless checks - sed -i 's|check-texinfo: $(syntax_checks)|check-texinfo:|' doc/local.mk + "${SED}" -i 's|check-texinfo: $(syntax_checks)|check-texinfo:|' doc/local.mk ./bootstrap --skip-po ./configure --quiet --disable-gcc-warnings --disable-nls --disable-dependency-tracking --disable-bold-man-page-references \ "$([ "${SELINUX_ENABLED}" = 1 ] && echo --with-selinux || echo --without-selinux)" #Add timeout to to protect against hangs - sed -i 's|^"\$@|'"${SYSTEM_TIMEOUT}"' 600 "\$@|' build-aux/test-driver - sed -i 's| tr | /usr/bin/tr |' tests/init.sh + "${SED}" -i 's|^"\$@|'"${SYSTEM_TIMEOUT}"' 600 "\$@|' build-aux/test-driver + "${SED}" -i 's| tr | /usr/bin/tr |' tests/init.sh # Use a better diff - sed -i 's|diff -c|diff -u|g' tests/Coreutils.pm + "${SED}" -i 's|diff -c|diff -u|g' tests/Coreutils.pm "${MAKE}" -j "$("${NPROC}")" # Handle generated factor tests @@ -152,152 +152,150 @@ else ) for i in ${seq}; do echo "strip t${i}.sh from Makefile" - sed -i -e "s/\$(tf)\/t${i}.sh//g" Makefile + "${SED}" -i -e "s/\$(tf)\/t${i}.sh//g" Makefile done # Remove tests checking for --version & --help # Not really interesting for us and logs are too big - sed -i -e '/tests\/help\/help-version.sh/ D' \ + "${SED}" -i -e '/tests\/help\/help-version.sh/ D' \ -e '/tests\/help\/help-version-getopt.sh/ D' \ Makefile touch gnu-built fi -grep -rl 'path_prepend_' tests/* | xargs -r sed -i 's| path_prepend_ ./src||' +grep -rl 'path_prepend_' tests/* | xargs -r "${SED}" -i 's| path_prepend_ ./src||' # path_prepend_ sets $abs_path_dir_: set it manually instead. -grep -rl '\$abs_path_dir_' tests/*/*.sh | xargs -r sed -i "s|\$abs_path_dir_|${UU_BUILD_DIR//\//\\/}|g" +grep -rl '\$abs_path_dir_' tests/*/*.sh | xargs -r "${SED}" -i "s|\$abs_path_dir_|${UU_BUILD_DIR//\//\\/}|g" # Use the system coreutils where the test fails due to error in a util that is not the one being tested -sed -i "s|grep '^#define HAVE_CAP 1' \$CONFIG_HEADER > /dev/null|true|" tests/ls/capability.sh +"${SED}" -i "s|grep '^#define HAVE_CAP 1' \$CONFIG_HEADER > /dev/null|true|" tests/ls/capability.sh # our messages are better -sed -i "s|cannot stat 'symlink': Permission denied|not writing through dangling symlink 'symlink'|" tests/cp/fail-perm.sh -sed -i "s|cp: target directory 'symlink': Permission denied|cp: 'symlink' is not a directory|" tests/cp/fail-perm.sh +"${SED}" -i "s|cannot stat 'symlink': Permission denied|not writing through dangling symlink 'symlink'|" tests/cp/fail-perm.sh +"${SED}" -i "s|cp: target directory 'symlink': Permission denied|cp: 'symlink' is not a directory|" tests/cp/fail-perm.sh # Our message is a bit better -sed -i "s|cannot create regular file 'no-such/': Not a directory|'no-such/' is not a directory|" tests/mv/trailing-slash.sh +"${SED}" -i "s|cannot create regular file 'no-such/': Not a directory|'no-such/' is not a directory|" tests/mv/trailing-slash.sh # Our message is better -sed -i "s|warning: unrecognized escape|warning: incomplete hex escape|" tests/stat/stat-printf.pl +"${SED}" -i "s|warning: unrecognized escape|warning: incomplete hex escape|" tests/stat/stat-printf.pl -sed -i 's|timeout |'"${SYSTEM_TIMEOUT}"' |' tests/tail/follow-stdin.sh +"${SED}" -i 's|timeout |'"${SYSTEM_TIMEOUT}"' |' tests/tail/follow-stdin.sh # trap_sigpipe_or_skip_ fails with uutils tools because of a bug in # timeout/yes (https://github.com/uutils/coreutils/issues/7252), so we use # system's yes/timeout to make sure the tests run (instead of being skipped). -sed -i 's|\(trap .* \)timeout\( .* \)yes|'"\1${SYSTEM_TIMEOUT}\2${SYSTEM_YES}"'|' init.cfg +"${SED}" -i 's|\(trap .* \)timeout\( .* \)yes|'"\1${SYSTEM_TIMEOUT}\2${SYSTEM_YES}"'|' init.cfg # Remove dup of /usr/bin/ and /usr/local/bin/ when executed several times -grep -rlE '/usr/bin/\s?/usr/bin' init.cfg tests/* | xargs -r sed -Ei 's|/usr/bin/\s?/usr/bin/|/usr/bin/|g' -grep -rlE '/usr/local/bin/\s?/usr/local/bin' init.cfg tests/* | xargs -r sed -Ei 's|/usr/local/bin/\s?/usr/local/bin/|/usr/local/bin/|g' +grep -rlE '/usr/bin/\s?/usr/bin' init.cfg tests/* | xargs -r "${SED}" -Ei 's|/usr/bin/\s?/usr/bin/|/usr/bin/|g' +grep -rlE '/usr/local/bin/\s?/usr/local/bin' init.cfg tests/* | xargs -r "${SED}" -Ei 's|/usr/local/bin/\s?/usr/local/bin/|/usr/local/bin/|g' #### Adjust tests to make them work with Rust/coreutils # in some cases, what we are doing in rust/coreutils is good (or better) # we should not regress our project just to match what GNU is going. # So, do some changes on the fly -sed -i -e "s|removed directory 'a/'|removed directory 'a'|g" tests/rm/v-slash.sh +"${SED}" -i -e "s|removed directory 'a/'|removed directory 'a'|g" tests/rm/v-slash.sh # 'rel' doesn't exist. Our implementation is giving a better message. -sed -i -e "s|rm: cannot remove 'rel': Permission denied|rm: cannot remove 'rel': No such file or directory|g" tests/rm/inaccessible.sh +"${SED}" -i -e "s|rm: cannot remove 'rel': Permission denied|rm: cannot remove 'rel': No such file or directory|g" tests/rm/inaccessible.sh # Our implementation shows "Directory not empty" for directories that can't be accessed due to lack of execute permissions # This is actually more accurate than "Permission denied" since the real issue is that we can't empty the directory -sed -i -e "s|rm: cannot remove 'a/1': Permission denied|rm: cannot remove 'a/1/2': Permission denied|g" -e "s|rm: cannot remove 'b': Permission denied|rm: cannot remove 'a': Directory not empty\nrm: cannot remove 'b/3': Permission denied|g" tests/rm/rm2.sh +"${SED}" -i -e "s|rm: cannot remove 'a/1': Permission denied|rm: cannot remove 'a/1/2': Permission denied|g" -e "s|rm: cannot remove 'b': Permission denied|rm: cannot remove 'a': Directory not empty\nrm: cannot remove 'b/3': Permission denied|g" tests/rm/rm2.sh # overlay-headers.sh test intends to check for inotify events, # however there's a bug because `---dis` is an alias for: `---disable-inotify` sed -i -e "s|---dis ||g" tests/tail/overlay-headers.sh # Do not FAIL, just do a regular ERROR -sed -i -e "s|framework_failure_ 'no inotify_add_watch';|fail=1;|" tests/tail/inotify-rotate-resources.sh +"${SED}" -i -e "s|framework_failure_ 'no inotify_add_watch';|fail=1;|" tests/tail/inotify-rotate-resources.sh test -f "${UU_BUILD_DIR}/getlimits" || cp src/getlimits "${UU_BUILD_DIR}" # pr produces very long log and this command isn't super interesting # SKIP for now -sed -i -e "s|my \$prog = 'pr';$|my \$prog = 'pr';CuSkip::skip \"\$prog: SKIP for producing too long logs\";|" tests/pr/pr-tests.pl +"${SED}" -i -e "s|my \$prog = 'pr';$|my \$prog = 'pr';CuSkip::skip \"\$prog: SKIP for producing too long logs\";|" tests/pr/pr-tests.pl # We don't have the same error message and no need to be that specific -sed -i -e "s|invalid suffix in --pages argument|invalid --pages argument|" \ +"${SED}" -i -e "s|invalid suffix in --pages argument|invalid --pages argument|" \ -e "s|--pages argument '\$too_big' too large|invalid --pages argument '\$too_big'|" \ -e "s|invalid page range|invalid --pages argument|" tests/misc/xstrtol.pl # When decoding an invalid base32/64 string, gnu writes everything it was able to decode until # it hit the decode error, while we don't write anything if the input is invalid. -sed -i "s/\(baddecode.*OUT=>\"\).*\"/\1\"/g" tests/basenc/base64.pl -sed -i "s/\(\(b2[ml]_[69]\|z85_8\|z85_35\).*OUT=>\)[^}]*\(.*\)/\1\"\"\3/g" tests/basenc/basenc.pl +"${SED}" -i "s/\(baddecode.*OUT=>\"\).*\"/\1\"/g" tests/basenc/base64.pl +"${SED}" -i "s/\(\(b2[ml]_[69]\|z85_8\|z85_35\).*OUT=>\)[^}]*\(.*\)/\1\"\"\3/g" tests/basenc/basenc.pl # add "error: " to the expected error message -sed -i "s/\$prog: invalid input/\$prog: error: invalid input/g" tests/basenc/basenc.pl +"${SED}" -i "s/\$prog: invalid input/\$prog: error: invalid input/g" tests/basenc/basenc.pl # basenc: swap out error message for unexpected arg -sed -i "s/ {ERR=>\"\$prog: foobar\\\\n\" \. \$try_help }/ {ERR=>\"error: unexpected argument '--foobar' found\n\n tip: to pass '--foobar' as a value, use '-- --foobar'\n\nUsage: basenc [OPTION]... [FILE]\n\nFor more information, try '--help'.\n\"}]/" tests/basenc/basenc.pl -sed -i "s/ {ERR_SUBST=>\"s\/(unrecognized|unknown) option \[-' \]\*foobar\[' \]\*\/foobar\/\"}],//" tests/basenc/basenc.pl +"${SED}" -i "s/ {ERR=>\"\$prog: foobar\\\\n\" \. \$try_help }/ {ERR=>\"error: unexpected argument '--foobar' found\n\n tip: to pass '--foobar' as a value, use '-- --foobar'\n\nUsage: basenc [OPTION]... [FILE]\n\nFor more information, try '--help'.\n\"}]/" tests/basenc/basenc.pl +"${SED}" -i "s/ {ERR_SUBST=>\"s\/(unrecognized|unknown) option \[-' \]\*foobar\[' \]\*\/foobar\/\"}],//" tests/basenc/basenc.pl # Remove the check whether a util was built. Otherwise tests against utils like "arch" are not run. -sed -i "s|require_built_ |# require_built_ |g" init.cfg +"${SED}" -i "s|require_built_ |# require_built_ |g" init.cfg # exit early for the selinux check. The first is enough for us. -sed -i "s|# Independent of whether SELinux|return 0\n #|g" init.cfg +"${SED}" -i "s|# Independent of whether SELinux|return 0\n #|g" init.cfg # Some tests are executed with the "nobody" user. # The check to verify if it works is based on the GNU coreutils version # making it too restrictive for us -sed -i "s|\$PACKAGE_VERSION|[0-9]*|g" tests/rm/fail-2eperm.sh tests/mv/sticky-to-xpart.sh init.cfg +"${SED}" -i "s|\$PACKAGE_VERSION|[0-9]*|g" tests/rm/fail-2eperm.sh tests/mv/sticky-to-xpart.sh init.cfg # usage_vs_getopt.sh is heavily modified as it runs all the binaries # with the option -/ is used, clap is returning a better error than GNU's. Adjust the GNU test -sed -i -e "s~ grep \" '\*/'\*\" err || framework_failure_~ grep \" '*-/'*\" err || framework_failure_~" tests/misc/usage_vs_getopt.sh -sed -i -e "s~ sed -n \"1s/'\\\/'/'OPT'/p\" < err >> pat || framework_failure_~ sed -n \"1s/'-\\\/'/'OPT'/p\" < err >> pat || framework_failure_~" tests/misc/usage_vs_getopt.sh +"${SED}" -i -e "s~ grep \" '\*/'\*\" err || framework_failure_~ grep \" '*-/'*\" err || framework_failure_~" tests/misc/usage_vs_getopt.sh +"${SED}" -i -e "s~ sed -n \"1s/'\\\/'/'OPT'/p\" < err >> pat || framework_failure_~ sed -n \"1s/'-\\\/'/'OPT'/p\" < err >> pat || framework_failure_~" tests/misc/usage_vs_getopt.sh # Ignore runcon, it needs some extra attention # For all other tools, we want drop-in compatibility, and that includes the exit code. -sed -i -e "s/rcexp=1$/rcexp=1\n case \"\$prg\" in runcon|stdbuf) return;; esac/" tests/misc/usage_vs_getopt.sh +"${SED}" -i -e "s/rcexp=1$/rcexp=1\n case \"\$prg\" in runcon|stdbuf) return;; esac/" tests/misc/usage_vs_getopt.sh # GNU has option=[SUFFIX], clap is -sed -i -e "s/cat opts/sed -i -e \"s| <.\*$||g\" opts/" tests/misc/usage_vs_getopt.sh +"${SED}" -i -e "s/cat opts/sed -i -e \"s| <.\*$||g\" opts/" tests/misc/usage_vs_getopt.sh # for some reasons, some stuff are duplicated, strip that -sed -i -e "s/provoked error./provoked error\ncat pat |sort -u > pat/" tests/misc/usage_vs_getopt.sh +"${SED}" -i -e "s/provoked error./provoked error\ncat pat |sort -u > pat/" tests/misc/usage_vs_getopt.sh # install verbose messages shows ginstall as command -sed -i -e "s/ginstall: creating directory/install: creating directory/g" tests/install/basic-1.sh +"${SED}" -i -e "s/ginstall: creating directory/install: creating directory/g" tests/install/basic-1.sh # GNU doesn't support padding < -LONG_MAX # disable this test case -# Use GNU sed because option -z is not available on BSD sed "${SED}" -i -Ez "s/\n([^\n#]*pad-3\.2[^\n]*)\n([^\n]*)\n([^\n]*)/\n# uutils\/numfmt supports padding = LONG_MIN\n#\1\n#\2\n#\3/" tests/numfmt/numfmt.pl # Update the GNU error message to match the one generated by clap -sed -i -e "s/\$prog: multiple field specifications/error: the argument '--field ' cannot be used multiple times\n\nUsage: numfmt [OPTION]... [NUMBER]...\n\nFor more information, try '--help'./g" tests/numfmt/numfmt.pl -sed -i -e "s/Try 'mv --help' for more information/For more information, try '--help'/g" -e "s/mv: missing file operand/error: the following required arguments were not provided:\n ...\n\nUsage: mv [OPTION]... [-T] SOURCE DEST\n mv [OPTION]... SOURCE... DIRECTORY\n mv [OPTION]... -t DIRECTORY SOURCE...\n/g" -e "s/mv: missing destination file operand after 'no-file'/error: The argument '...' requires at least 2 values, but only 1 was provided\n\nUsage: mv [OPTION]... [-T] SOURCE DEST\n mv [OPTION]... SOURCE... DIRECTORY\n mv [OPTION]... -t DIRECTORY SOURCE...\n/g" tests/mv/diag.sh +"${SED}" -i -e "s/\$prog: multiple field specifications/error: the argument '--field ' cannot be used multiple times\n\nUsage: numfmt [OPTION]... [NUMBER]...\n\nFor more information, try '--help'./g" tests/numfmt/numfmt.pl +"${SED}" -i -e "s/Try 'mv --help' for more information/For more information, try '--help'/g" -e "s/mv: missing file operand/error: the following required arguments were not provided:\n ...\n\nUsage: mv [OPTION]... [-T] SOURCE DEST\n mv [OPTION]... SOURCE... DIRECTORY\n mv [OPTION]... -t DIRECTORY SOURCE...\n/g" -e "s/mv: missing destination file operand after 'no-file'/error: The argument '...' requires at least 2 values, but only 1 was provided\n\nUsage: mv [OPTION]... [-T] SOURCE DEST\n mv [OPTION]... SOURCE... DIRECTORY\n mv [OPTION]... -t DIRECTORY SOURCE...\n/g" tests/mv/diag.sh # our error message is better -sed -i -e "s|mv: cannot overwrite 'a/t': Directory not empty|mv: cannot move 'b/t' to 'a/t': Directory not empty|" tests/mv/dir2dir.sh +"${SED}" -i -e "s|mv: cannot overwrite 'a/t': Directory not empty|mv: cannot move 'b/t' to 'a/t': Directory not empty|" tests/mv/dir2dir.sh # GNU doesn't support width > INT_MAX # disable these test cases -sed -i -E "s|^([^#]*2_31.*)$|#\1|g" tests/printf/printf-cov.pl +"${SED}" -i -E "s|^([^#]*2_31.*)$|#\1|g" tests/printf/printf-cov.pl -sed -i -e "s/du: invalid -t argument/du: invalid --threshold argument/" -e "s/du: option requires an argument/error: a value is required for '--threshold ' but none was supplied/" -e "s/Try 'du --help' for more information./\nFor more information, try '--help'./" tests/du/threshold.sh +"${SED}" -i -e "s/du: invalid -t argument/du: invalid --threshold argument/" -e "s/du: option requires an argument/error: a value is required for '--threshold ' but none was supplied/" -e "s/Try 'du --help' for more information./\nFor more information, try '--help'./" tests/du/threshold.sh # Remove the extra output check -sed -i -e "s|Try '\$prog --help' for more information.\\\n||" tests/du/files0-from.pl -sed -i -e "s|when reading file names from stdin, no file name of\"|-: No such file or directory\n\"|" -e "s| '-' allowed\\\n||" tests/du/files0-from.pl -sed -i -e "s|-: No such file or directory|cannot access '-': No such file or directory|g" tests/du/files0-from.pl +"${SED}" -i -e "s|Try '\$prog --help' for more information.\\\n||" tests/du/files0-from.pl +"${SED}" -i -e "s|when reading file names from stdin, no file name of\"|-: No such file or directory\n\"|" -e "s| '-' allowed\\\n||" tests/du/files0-from.pl +"${SED}" -i -e "s|-: No such file or directory|cannot access '-': No such file or directory|g" tests/du/files0-from.pl # Skip the move-dir-while-traversing test - our implementation uses safe traversal with openat() # which avoids the TOCTOU race condition that this test tries to trigger. The test uses inotify # to detect when du opens a directory path and moves it to cause an error, but our openat-based # implementation doesn't trigger inotify events on the full path, preventing the race condition. # This is actually better behavior - we're immune to this class of filesystem race attacks. -sed -i '1s/^/exit 0 # Skip test - uutils du uses safe traversal that prevents this race condition\n/' tests/du/move-dir-while-traversing.sh +"${SED}" -i '1s/^/exit 0 # Skip test - uutils du uses safe traversal that prevents this race condition\n/' tests/du/move-dir-while-traversing.sh awk 'BEGIN {count=0} /compare exp out2/ && count < 6 {sub(/compare exp out2/, "grep -q \"cannot be used with\" out2"); count++} 1' tests/df/df-output.sh > tests/df/df-output.sh.tmp && mv tests/df/df-output.sh.tmp tests/df/df-output.sh # with ls --dired, in case of error, we have a slightly different error position -sed -i -e "s|44 45|48 49|" tests/ls/stat-failed.sh +"${SED}" -i -e "s|44 45|48 49|" tests/ls/stat-failed.sh # small difference in the error message -# Use GNU sed for /c command "${SED}" -i -e "s/ls: invalid argument 'XX' for 'time style'/ls: invalid --time-style argument 'XX'/" \ -e "s/Valid arguments are:/Possible values are:/" \ -e "s/Try 'ls --help' for more information./\nFor more information try --help/" \ @@ -306,30 +304,29 @@ sed -i -e "s|44 45|48 49|" tests/ls/stat-failed.sh # disable two kind of tests: # "hostid BEFORE --help" doesn't fail for GNU. we fail. we are probably doing better # "hostid BEFORE --help AFTER " same for this -sed -i -e "s/env \$prog \$BEFORE \$opt > out2/env \$prog \$BEFORE \$opt > out2 #/" -e "s/env \$prog \$BEFORE \$opt AFTER > out3/env \$prog \$BEFORE \$opt AFTER > out3 #/" -e "s/compare exp out2/compare exp out2 #/" -e "s/compare exp out3/compare exp out3 #/" tests/help/help-version-getopt.sh +"${SED}" -i -e "s/env \$prog \$BEFORE \$opt > out2/env \$prog \$BEFORE \$opt > out2 #/" -e "s/env \$prog \$BEFORE \$opt AFTER > out3/env \$prog \$BEFORE \$opt AFTER > out3 #/" -e "s/compare exp out2/compare exp out2 #/" -e "s/compare exp out3/compare exp out3 #/" tests/help/help-version-getopt.sh # Add debug info + we have less syscall then GNU's. Adjust our check. -# Use GNU sed for /c command "${SED}" -i -e '/test \$n_stat1 = \$n_stat2 \\/c\ 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 -sed -i -e "s|Try 'md5sum --help' for more information.\\\n||" tests/cksum/md5sum.pl +"${SED}" -i -e "s|Try 'md5sum --help' for more information.\\\n||" 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. -sed -i -E 's/\^\[\[([1-9]m)/^[[0\1/g; s/\^\[\[m/^[[0m/g' tests/ls/color-norm.sh +"${SED}" -i -E 's/\^\[\[([1-9]m)/^[[0\1/g; s/\^\[\[m/^[[0m/g' tests/ls/color-norm.sh # It says in the test itself that having more than one reset is a bug, so we # don't need to replicate that behavior. -sed -i -E 's/(\^\[\[0m)+/\^\[\[0m/g' tests/ls/color-norm.sh +"${SED}" -i -E 's/(\^\[\[0m)+/\^\[\[0m/g' tests/ls/color-norm.sh # GNU's ls seems to output color codes in the order given in the environment # variable, but our ls seems to output them in a predefined order. Nevertheless, # the order doesn't matter, so it's okay. -sed -i 's/44;37/37;44/' tests/ls/multihardlink.sh +"${SED}" -i 's/44;37/37;44/' tests/ls/multihardlink.sh # Just like mentioned in the previous patch, GNU's ls output color codes in the # same way it is specified in the environment variable, but our ls emits them @@ -338,24 +335,24 @@ sed -i 's/44;37/37;44/' tests/ls/multihardlink.sh # individually, for example, ^[[31^[[42 instead of ^[[31;42, but we don't do # that anywhere in our implementation, and it looks like GNU's ls also doesn't # do that. So, it's okay to ignore the zero. -sed -i "s/color_code='0;31;42'/color_code='31;42'/" tests/ls/color-clear-to-eol.sh +"${SED}" -i "s/color_code='0;31;42'/color_code='31;42'/" tests/ls/color-clear-to-eol.sh # patching this because of the same reason as the last one. -sed -i "s/color_code='0;31;42'/color_code='31;42'/" tests/ls/quote-align.sh +"${SED}" -i "s/color_code='0;31;42'/color_code='31;42'/" tests/ls/quote-align.sh # Slightly different error message -sed -i 's/not supported/unexpected argument/' tests/mv/mv-exchange.sh +"${SED}" -i 's/not supported/unexpected argument/' tests/mv/mv-exchange.sh # Most tests check that `/usr/bin/tr` is working correctly before running. # However in NixOS/Nix-based distros, the tr util is located somewhere in # /nix/store/xxxxxxxxxxxx...xxxx/bin/tr # We just replace the references to `/usr/bin/tr` -sed -i 's/\/usr\/bin\/tr/$(command -v tr)/' tests/init.sh +"${SED}" -i 's/\/usr\/bin\/tr/$(command -v tr)/' tests/init.sh # upstream doesn't having the program name in the error message # but we do. We should keep it that way. -sed -i 's/echo "changing security context/echo "chcon: changing security context/' tests/chcon/chcon.sh +"${SED}" -i 's/echo "changing security context/echo "chcon: changing security context/' tests/chcon/chcon.sh # Disable this test, it is not relevant for us: # * the selinux crate is handling errors # * the test says "maybe we should not fail when no context available" -sed -i -e "s|returns_ 1||g" tests/cp/no-ctx.sh +"${SED}" -i -e "s|returns_ 1||g" tests/cp/no-ctx.sh From fef95fc5dda777158615bf8c65f8f24662583096 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Mon, 24 Nov 2025 17:57:50 +0900 Subject: [PATCH 108/182] l10n.yml:Don't apt-get build-essential --- .github/workflows/l10n.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/l10n.yml b/.github/workflows/l10n.yml index 9d6821738..8d82c7f2c 100644 --- a/.github/workflows/l10n.yml +++ b/.github/workflows/l10n.yml @@ -429,7 +429,7 @@ jobs: ## Install/setup prerequisites case '${{ matrix.job.os }}' in ubuntu-*) - sudo apt-get -y update ; sudo apt-get -y install libselinux1-dev build-essential + sudo apt-get -y update ; sudo apt-get -y install libselinux1-dev ;; macos-*) brew install coreutils make @@ -580,7 +580,7 @@ jobs: ## Install/setup prerequisites case '${{ matrix.job.os }}' in ubuntu-*) - sudo apt-get -y update ; sudo apt-get -y install libselinux1-dev build-essential + sudo apt-get -y update ; sudo apt-get -y install libselinux1-dev # Generate French locale for testing sudo locale-gen --keep-existing fr_FR.UTF-8 locale -a | grep -i fr || echo "French locale generation may have failed" @@ -912,7 +912,7 @@ jobs: run: | ## Install/setup prerequisites including locale support sudo apt-get -y update - sudo apt-get -y install libselinux1-dev build-essential + sudo apt-get -y install libselinux1-dev # Generate multiple locales for testing sudo locale-gen --keep-existing en_US.UTF-8 fr_FR.UTF-8 de_DE.UTF-8 es_ES.UTF-8 @@ -1160,7 +1160,7 @@ jobs: # Use different cache key for each build to avoid conflicts key: cat-locale-embedding - name: Install prerequisites - run: sudo apt-get -y update && sudo apt-get -y install libselinux1-dev build-essential + run: sudo apt-get -y update && sudo apt-get -y install libselinux1-dev - name: Build cat with targeted locale embedding run: UUCORE_TARGET_UTIL=cat cargo build -p uu_cat --release - name: Verify cat locale count @@ -1192,7 +1192,7 @@ jobs: # Use different cache key for each build to avoid conflicts key: ls-locale-embedding - name: Install prerequisites - run: sudo apt-get -y update && sudo apt-get -y install libselinux1-dev build-essential + run: sudo apt-get -y update && sudo apt-get -y install libselinux1-dev - name: Build ls with targeted locale embedding run: UUCORE_TARGET_UTIL=ls cargo build -p uu_ls --release - name: Verify ls locale count @@ -1224,7 +1224,7 @@ jobs: # Use different cache key for each build to avoid conflicts key: multicall-locale-embedding - name: Install prerequisites - run: sudo apt-get -y update && sudo apt-get -y install libselinux1-dev build-essential + run: sudo apt-get -y update && sudo apt-get -y install libselinux1-dev - name: Build multicall binary with all locales run: cargo build --release - name: Verify multicall locale count From c8e619ab5e82a80a25cce682b46d2d4dda8f185b Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Mon, 24 Nov 2025 18:20:17 +0900 Subject: [PATCH 109/182] l10n.yml: Use PROFILE=release-small for faster CI --- .github/workflows/l10n.yml | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/.github/workflows/l10n.yml b/.github/workflows/l10n.yml index 9d6821738..f8f1cd520 100644 --- a/.github/workflows/l10n.yml +++ b/.github/workflows/l10n.yml @@ -453,22 +453,22 @@ jobs: # First check if binary exists after build echo "Checking if coreutils was built..." - ls -la target/release/coreutils || echo "No coreutils binary in target/release/" + ls -la target/release-small/coreutils || echo "No coreutils binary in target/release-small/" - make FEATURES="${{ matrix.job.features }}" PROFILE=release MULTICALL=y + make FEATURES="${{ matrix.job.features }}" PROFILE=release-small MULTICALL=y - echo "After build, checking target/release/:" - ls -la target/release/ | grep -E "(coreutils|^total)" || echo "Build may have failed" + echo "After build, checking target/release-small/:" + ls -la target/release-small/ | grep -E "(coreutils|^total)" || echo "Build may have failed" echo "Running make install..." echo "Before install - checking what we have:" - ls -la target/release/coreutils 2>/dev/null || echo "No coreutils in target/release" + ls -la target/release-small/coreutils 2>/dev/null || echo "No coreutils in target/release-small" # Run make install with verbose output to see what happens - echo "About to run: make install DESTDIR=\"$INSTALL_DIR\" PREFIX=/usr PROFILE=release MULTICALL=y" + echo "About to run: make install DESTDIR=\"$INSTALL_DIR\" PREFIX=/usr PROFILE=release-small MULTICALL=y" echo "Expected install path: $INSTALL_DIR/usr/bin/coreutils" - make install DESTDIR="$INSTALL_DIR" PREFIX=/usr PROFILE=release MULTICALL=y || { + make install DESTDIR="$INSTALL_DIR" PREFIX=/usr PROFILE=release-small MULTICALL=y || { echo "Make install failed! Exit code: $?" echo "Let's see what happened:" ls -la "$INSTALL_DIR" 2>/dev/null || echo "Install directory doesn't exist" @@ -482,13 +482,13 @@ jobs: echo "Current directory: $(pwd)" echo "INSTALL_DIR: $INSTALL_DIR" echo "Checking if build succeeded..." - if [ -f "target/release/coreutils" ]; then - echo "✓ Build succeeded - coreutils binary exists in target/release/" - ls -la target/release/coreutils + if [ -f "target/release-small/coreutils" ]; then + echo "✓ Build succeeded - coreutils binary exists in target/release-small/" + ls -la target/release-small/coreutils else - echo "✗ Build failed - no coreutils binary in target/release/" - echo "Contents of target/release/:" - ls -la target/release/ | head -20 + echo "✗ Build failed - no coreutils binary in target/release-small/" + echo "Contents of target/release-small/:" + ls -la target/release-small/ | head -20 exit 1 fi @@ -600,8 +600,8 @@ jobs: mkdir -p "$MAKE_INSTALL_DIR" # Build and install using make with DESTDIR - make FEATURES="${{ matrix.job.features }}" PROFILE=release MULTICALL=y - make install DESTDIR="$MAKE_INSTALL_DIR" PREFIX=/usr PROFILE=release MULTICALL=y + make FEATURES="${{ matrix.job.features }}" PROFILE=release-small MULTICALL=y + make install DESTDIR="$MAKE_INSTALL_DIR" PREFIX=/usr PROFILE=release-small MULTICALL=y # Verify installation echo "Testing make-installed binaries..." @@ -928,8 +928,8 @@ jobs: mkdir -p "$INSTALL_DIR" # Build and install using make with DESTDIR - make FEATURES="feat_os_unix" PROFILE=release MULTICALL=y - make install DESTDIR="$INSTALL_DIR" PREFIX=/usr PROFILE=release MULTICALL=y + make FEATURES="feat_os_unix" PROFILE=release-small MULTICALL=y + make install DESTDIR="$INSTALL_DIR" PREFIX=/usr PROFILE=release-small MULTICALL=y # Debug: Show what was installed echo "Contents of installation directory:" @@ -1109,8 +1109,8 @@ jobs: # Clean and build standard version make clean - make FEATURES="feat_os_unix" PROFILE=release MULTICALL=y - make install DESTDIR="$STANDARD_BUILD_INSTALL_DIR" PREFIX=/usr PROFILE=release MULTICALL=y + make FEATURES="feat_os_unix" PROFILE=release-small MULTICALL=y + make install DESTDIR="$STANDARD_BUILD_INSTALL_DIR" PREFIX=/usr PROFILE=release-small MULTICALL=y # Verify standard build binary works if "$STANDARD_BUILD_INSTALL_DIR/usr/bin/coreutils" --version >/dev/null 2>&1; then From 6a2b97273e3329b5eb76d7d9e4f67009853d1019 Mon Sep 17 00:00:00 2001 From: Chris Dryden Date: Mon, 24 Nov 2025 05:03:47 -0500 Subject: [PATCH 110/182] stty: baud parsing integration tests and validation (#9454) * Adding comprehensive gnu suite baud parsing rules * Adding missing spellcheck words * Fixed clippy errors and simplified rounding logic --- src/uu/stty/src/stty.rs | 79 ++++++++++++++++++++++++++++++++++---- tests/by-util/test_stty.rs | 18 +++++++++ 2 files changed, 89 insertions(+), 8 deletions(-) diff --git a/src/uu/stty/src/stty.rs b/src/uu/stty/src/stty.rs index fdeee252d..42432c22c 100644 --- a/src/uu/stty/src/stty.rs +++ b/src/uu/stty/src/stty.rs @@ -10,7 +10,7 @@ // spell-checker:ignore isig icanon iexten echoe crterase echok echonl noflsh xcase tostop echoprt prterase echoctl ctlecho echoke crtkill flusho extproc // spell-checker:ignore lnext rprnt susp swtch vdiscard veof veol verase vintr vkill vlnext vquit vreprint vstart vstop vsusp vswtc vwerase werase // spell-checker:ignore sigquit sigtstp -// spell-checker:ignore cbreak decctlq evenp litout oddp tcsadrain +// spell-checker:ignore cbreak decctlq evenp litout oddp tcsadrain exta extb mod flags; @@ -23,6 +23,7 @@ use nix::sys::termios::{ Termios, cfgetospeed, cfsetospeed, tcgetattr, tcsetattr, }; 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::num::IntErrorKind; @@ -563,7 +564,69 @@ fn string_to_combo(arg: &str) -> Option<&str> { .map(|_| arg) } +/// Parse and round a baud rate value using GNU stty's custom rounding algorithm. +/// +/// Accepts decimal values with the following rounding rules: +/// - If first digit after decimal > 5: round up +/// - If first digit after decimal < 5: round down +/// - If first digit after decimal == 5: +/// - If followed by any non-zero digit: round up +/// - If followed only by zeros (or nothing): banker's rounding (round to nearest even) +/// +/// Examples: "9600.49" -> 9600, "9600.51" -> 9600, "9600.5" -> 9600 (even), "9601.5" -> 9602 (even) +/// TODO: there are two special cases "exta" → B19200 and "extb" → B38400 +fn parse_baud_with_rounding(normalized: &str) -> Option { + let (int_part, frac_part) = match normalized.split_once('.') { + Some((i, f)) => (i, Some(f)), + None => (normalized, None), + }; + + let mut value = int_part.parse::().ok()?; + + if let Some(frac) = frac_part { + let mut chars = frac.chars(); + let first_digit = chars.next()?.to_digit(10)?; + + // Validate all remaining chars are digits + let rest: Vec<_> = chars.collect(); + if !rest.iter().all(|c| c.is_ascii_digit()) { + return None; + } + + match first_digit.cmp(&5) { + Ordering::Greater => value += 1, + Ordering::Equal => { + // Check if any non-zero digit follows + if rest.iter().any(|&c| c != '0') { + value += 1; + } else { + // Banker's rounding: round to nearest even + value += value & 1; + } + } + Ordering::Less => {} // Round down, already validated + } + } + + Some(value) +} + fn string_to_baud(arg: &str) -> Option> { + // Reject invalid formats + if arg != arg.trim_end() + || arg.trim().starts_with('-') + || arg.trim().starts_with("++") + || arg.contains('E') + || arg.contains('e') + || arg.matches('.').count() > 1 + { + return None; + } + + let normalized = arg.trim().trim_start_matches('+'); + let normalized = normalized.strip_suffix('.').unwrap_or(normalized); + let value = parse_baud_with_rounding(normalized)?; + // BSDs use a u32 for the baud rate, so any decimal number applies. #[cfg(any( target_os = "freebsd", @@ -573,9 +636,7 @@ fn string_to_baud(arg: &str) -> Option> { target_os = "netbsd", target_os = "openbsd" ))] - if let Ok(n) = arg.parse::() { - return Some(AllFlags::Baud(n)); - } + return Some(AllFlags::Baud(value)); #[cfg(not(any( target_os = "freebsd", @@ -585,12 +646,14 @@ fn string_to_baud(arg: &str) -> Option> { target_os = "netbsd", target_os = "openbsd" )))] - for (text, baud_rate) in BAUD_RATES { - if *text == arg { - return Some(AllFlags::Baud(*baud_rate)); + { + for (text, baud_rate) in BAUD_RATES { + if text.parse::().ok() == Some(value) { + return Some(AllFlags::Baud(*baud_rate)); + } } + None } - None } /// return `Some(flag)` if the input is a valid flag, `None` if not diff --git a/tests/by-util/test_stty.rs b/tests/by-util/test_stty.rs index d6870d48f..9626c1406 100644 --- a/tests/by-util/test_stty.rs +++ b/tests/by-util/test_stty.rs @@ -194,6 +194,24 @@ fn invalid_baud_setting() { .args(&["ospeed", "995"]) .fails() .stderr_contains("invalid ospeed '995'"); + + for speed in &[ + "9599..", "9600..", "9600.5.", "9600.50.", "9600.0.", "++9600", "0x2580", "96E2", "9600,0", + "9600.0 ", + ] { + new_ucmd!().args(&["ispeed", speed]).fails(); + } +} + +#[test] +#[cfg(unix)] +fn valid_baud_formats() { + let (path, _controller, _replica) = pty_path(); + for speed in &[" +9600", "9600.49", "9600.50", "9599.51", " 9600."] { + new_ucmd!() + .args(&["--file", &path, "ispeed", speed]) + .succeeds(); + } } #[test] From 3cd4c21b24ba2214aa27d414f64f040d35ca8c94 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Mon, 24 Nov 2025 20:08:42 +0900 Subject: [PATCH 111/182] l10n.yml: Do not brew make --- .github/workflows/l10n.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/l10n.yml b/.github/workflows/l10n.yml index edaf323d0..c7154f490 100644 --- a/.github/workflows/l10n.yml +++ b/.github/workflows/l10n.yml @@ -432,7 +432,7 @@ jobs: sudo apt-get -y update ; sudo apt-get -y install libselinux1-dev ;; macos-*) - brew install coreutils make + brew install coreutils ;; esac - name: Install via make and test multi-call binary @@ -586,7 +586,7 @@ jobs: locale -a | grep -i fr || echo "French locale generation may have failed" ;; macos-*) - brew install coreutils make + brew install coreutils ;; esac - name: Test Make installation From 8d740257ac1b1152bc58596f08462ab81e760e6c Mon Sep 17 00:00:00 2001 From: naoNao89 <90588855+naoNao89@users.noreply.github.com> Date: Fri, 14 Nov 2025 22:19:22 +0700 Subject: [PATCH 112/182] feat(uucore): add shared hardware detection module Add shared CPU hardware capability detection in uucore to prevent code duplication across utilities. This provides a unified interface for detecting CPU features (AVX512, AVX2, PCLMUL, SSE2, ASIMD) and respecting GLIBC_TUNABLES environment variable. This unblocks PR #9088 (cksum --debug) and PR #9144 (wc --debug) by providing a common implementation that both utilities can use. Features: - CPU feature detection with caching (singleton pattern) - GLIBC_TUNABLES parsing for hwcaps restrictions - Cross-platform support (x86/x86_64, aarch64) - Comprehensive test coverage - Zero-cost abstractions using std::arch Implementation details: - Uses std::arch feature detection (no external deps for detection) - Adds cfg-if dependency for conditional compilation - Feature-gated behind "hardware" feature flag - Android excluded (no CPUID access in sandboxed environment) Related: #9088, #9144 --- .../cspell.dictionaries/jargon.wordlist.txt | 13 + fuzz/Cargo.lock | 188 ++------ src/uucore/Cargo.toml | 1 + src/uucore/src/lib/features.rs | 2 + src/uucore/src/lib/features/hardware.rs | 433 ++++++++++++++++++ src/uucore/src/lib/lib.rs | 2 + 6 files changed, 495 insertions(+), 144 deletions(-) create mode 100644 src/uucore/src/lib/features/hardware.rs diff --git a/.vscode/cspell.dictionaries/jargon.wordlist.txt b/.vscode/cspell.dictionaries/jargon.wordlist.txt index a3b51bfed..0806d14ba 100644 --- a/.vscode/cspell.dictionaries/jargon.wordlist.txt +++ b/.vscode/cspell.dictionaries/jargon.wordlist.txt @@ -188,3 +188,16 @@ nofield # * clippy uninlined nonminimal + +# * CPU/hardware features +ASIMD +asimd +hwcaps +PCLMUL +pclmul +PCLMULQDQ +pclmulqdq +TUNABLES +tunables +VMULL +vmull diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index 58ee595df..f224e0437 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -8,15 +8,6 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - [[package]] name = "android_system_properties" version = "0.1.5" @@ -58,22 +49,22 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.10" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -205,9 +196,9 @@ checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" [[package]] name = "cc" -version = "1.2.44" +version = "1.2.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37521ac7aabe3d13122dc382493e20c9416f299d2ccd5b3a5340a2570cdeb0f3" +checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36" dependencies = [ "find-msvc-tools", "jobserver", @@ -349,15 +340,14 @@ checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "crc-fast" -version = "1.6.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ddc2d09feefeee8bd78101665bd8645637828fa9317f9f292496dbbd8c65ff3" +checksum = "a2f7c8d397a6353ef0c1d6217ab91b3ddb5431daf57fd013f506b967dcf44458" dependencies = [ "crc", "digest", - "rand", - "regex", "rustversion", + "spin", ] [[package]] @@ -402,9 +392,9 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "typenum", @@ -525,9 +515,9 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "find-msvc-tools" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" [[package]] name = "flate2" @@ -592,9 +582,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "generic-array" -version = "0.14.9" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", @@ -834,24 +824,24 @@ dependencies = [ [[package]] name = "jiff" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" +checksum = "49cce2b81f2098e7e3efc35bc2e0a6b7abec9d34128283d7a26fa8f32a6dbb35" dependencies = [ "jiff-static", "jiff-tzdb-platform", "log", "portable-atomic", "portable-atomic-util", - "serde", - "windows-sys 0.59.0", + "serde_core", + "windows-sys 0.61.2", ] [[package]] name = "jiff-static" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" +checksum = "980af8b43c3ad5d8d349ace167ec8170839f753a42d233ba19e08afe1850fa69" dependencies = [ "proc-macro2", "quote", @@ -1093,9 +1083,9 @@ checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" [[package]] name = "parse_datetime" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77d45119ed61100f40b2389d8ed12e51ec869046d4279afbb5a7c73a4733be36" +checksum = "e4955561bc7aa4c40afcfd2a8c34297b13164ae9ac3b30ac348737befdc98e4c" dependencies = [ "jiff", "num-traits", @@ -1197,9 +1187,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.41" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] @@ -1259,34 +1249,11 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "regex" -version = "1.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - [[package]] name = "regex-automata" version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "rust-ini" @@ -1430,6 +1397,12 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "spin" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -1444,9 +1417,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.108" +version = "2.0.110" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" +checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" dependencies = [ "proc-macro2", "quote", @@ -1993,22 +1966,13 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-sys" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.5", + "windows-targets", ] [[package]] @@ -2020,22 +1984,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - [[package]] name = "windows-targets" version = "0.53.5" @@ -2043,106 +1991,58 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - [[package]] name = "windows_aarch64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - [[package]] name = "windows_aarch64_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - [[package]] name = "windows_i686_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - [[package]] name = "windows_i686_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - [[package]] name = "windows_i686_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - [[package]] name = "windows_x86_64_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - [[package]] name = "windows_x86_64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - [[package]] name = "windows_x86_64_msvc" version = "0.53.1" diff --git a/src/uucore/Cargo.toml b/src/uucore/Cargo.toml index 242f25903..46b2f9daa 100644 --- a/src/uucore/Cargo.toml +++ b/src/uucore/Cargo.toml @@ -131,6 +131,7 @@ fast-inc = [] fs = ["dunce", "libc", "winapi-util", "windows-sys"] fsext = ["libc", "windows-sys"] fsxattr = ["xattr"] +hardware = [] lines = [] feat_systemd_logind = ["utmpx", "libc"] format = [ diff --git a/src/uucore/src/lib/features.rs b/src/uucore/src/lib/features.rs index ac03fb79d..6d239642a 100644 --- a/src/uucore/src/lib/features.rs +++ b/src/uucore/src/lib/features.rs @@ -74,6 +74,8 @@ pub mod tty; #[cfg(all(unix, feature = "fsxattr"))] pub mod fsxattr; +#[cfg(feature = "hardware")] +pub mod hardware; #[cfg(all(target_os = "linux", feature = "selinux"))] pub mod selinux; #[cfg(all(unix, not(target_os = "fuchsia"), feature = "signals"))] diff --git a/src/uucore/src/lib/features/hardware.rs b/src/uucore/src/lib/features/hardware.rs new file mode 100644 index 000000000..e0325ed2f --- /dev/null +++ b/src/uucore/src/lib/features/hardware.rs @@ -0,0 +1,433 @@ +// 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. + +//! CPU hardware capability detection for performance-sensitive utilities +//! +//! This module provides a unified interface for detecting CPU features and +//! respecting environment-based SIMD policies (e.g., GLIBC_TUNABLES). +//! +//! # Use Cases +//! +//! - `cksum --debug`: Report hardware acceleration capabilities +//! - `wc --debug`: Report SIMD usage and GLIBC_TUNABLES restrictions +//! - Runtime decisions: Enable/disable SIMD paths based on environment +//! +//! # Examples +//! +//! ```no_run +//! use uucore::hardware::{CpuFeatures, simd_policy}; +//! +//! // Simple hardware detection +//! let features = CpuFeatures::detect(); +//! if features.has_avx2() { +//! println!("AVX2 is available"); +//! } +//! +//! // Check SIMD policy (respects GLIBC_TUNABLES) +//! let policy = simd_policy(); +//! if policy.allows_simd() { +//! // Use SIMD-accelerated path +//! } else { +//! // Fall back to scalar implementation +//! } +//! ``` + +use std::env; +use std::sync::OnceLock; + +/// CPU hardware features that affect performance +/// +/// Provides platform-specific CPU feature detection with caching. +/// Detection is performed once and cached for the lifetime of the process. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct CpuFeatures { + /// AVX-512 support (x86/x86_64 only) + avx512: bool, + /// AVX2 support (x86/x86_64 only) + avx2: bool, + /// PCLMULQDQ support for CRC acceleration (x86/x86_64 only) + pclmul: bool, + /// VMULL support for CRC acceleration (ARM only) + vmull: bool, + /// SSE2 support (x86/x86_64 only) + sse2: bool, + /// ARM ASIMD/NEON support (aarch64 only) + asimd: bool, +} + +impl CpuFeatures { + /// Detect available CPU features (cached after first call) + /// + /// This function uses a singleton pattern to ensure feature detection + /// happens only once per process. Thread-safe. + /// + /// # Examples + /// + /// ```no_run + /// use uucore::hardware::CpuFeatures; + /// + /// let features = CpuFeatures::detect(); + /// println!("AVX2: {}", features.has_avx2()); + /// ``` + pub fn detect() -> Self { + static FEATURES: OnceLock = OnceLock::new(); + *FEATURES.get_or_init(Self::detect_impl) + } + + fn detect_impl() -> Self { + Self { + avx512: detect_avx512(), + avx2: detect_avx2(), + pclmul: detect_pclmul(), + vmull: detect_vmull(), + sse2: detect_sse2(), + asimd: detect_asimd(), + } + } + + /// Check if AVX-512 is available (x86/x86_64 only) + pub fn has_avx512(&self) -> bool { + self.avx512 + } + + /// Check if AVX2 is available (x86/x86_64 only) + pub fn has_avx2(&self) -> bool { + self.avx2 + } + + /// Check if PCLMULQDQ is available (x86/x86_64 only) + pub fn has_pclmul(&self) -> bool { + self.pclmul + } + + /// Check if VMULL is available (ARM only) + pub fn has_vmull(&self) -> bool { + self.vmull + } + + /// Check if SSE2 is available (x86/x86_64 only) + pub fn has_sse2(&self) -> bool { + self.sse2 + } + + /// Check if ARM ASIMD/NEON is available (aarch64 only) + pub fn has_asimd(&self) -> bool { + self.asimd + } + + /// Get list of available features as strings + /// + /// Returns uppercase feature names (e.g., "AVX2", "SSE2", "ASIMD") + pub fn available_features(&self) -> Vec<&'static str> { + let mut features = Vec::new(); + if self.avx512 { + features.push("AVX512"); + } + if self.avx2 { + features.push("AVX2"); + } + if self.pclmul { + features.push("PCLMUL"); + } + if self.vmull { + features.push("VMULL"); + } + if self.sse2 { + features.push("SSE2"); + } + if self.asimd { + features.push("ASIMD"); + } + features + } +} + +/// SIMD policy based on environment variables +/// +/// Respects GLIBC_TUNABLES environment variable to disable specific CPU features. +/// This is used by GNU utilities to allow users to disable hardware acceleration. +#[derive(Debug, Clone)] +pub struct SimdPolicy { + /// Features disabled via GLIBC_TUNABLES (e.g., ["AVX2", "AVX512F"]) + disabled_by_env: Vec, + /// Hardware features actually available + hardware_features: CpuFeatures, +} + +impl SimdPolicy { + /// Create a new SIMD policy by checking environment and hardware + fn new() -> Self { + let tunables = env::var("GLIBC_TUNABLES").unwrap_or_default(); + let disabled_by_env = parse_disabled_features(&tunables); + let hardware_features = CpuFeatures::detect(); + + Self { + disabled_by_env, + hardware_features, + } + } + + /// Check if SIMD operations are allowed + /// + /// Returns `false` if any features are disabled via GLIBC_TUNABLES, + /// regardless of what's available in hardware. + /// + /// # Examples + /// + /// ```no_run + /// use uucore::hardware::simd_policy; + /// + /// let policy = simd_policy(); + /// if policy.allows_simd() { + /// // Use SIMD-accelerated bytecount + /// } else { + /// // Use scalar fallback + /// } + /// ``` + pub fn allows_simd(&self) -> bool { + self.disabled_by_env.is_empty() + } + + /// Get list of features disabled by environment + pub fn disabled_features(&self) -> &[String] { + &self.disabled_by_env + } + + /// Get available hardware features + pub fn hardware_features(&self) -> &CpuFeatures { + &self.hardware_features + } + + /// Get list of features that are both available and not disabled + pub fn enabled_features(&self) -> Vec<&'static str> { + if !self.allows_simd() { + return Vec::new(); + } + self.hardware_features.available_features() + } +} + +/// Get the global SIMD policy (cached) +/// +/// This checks both hardware capabilities and the GLIBC_TUNABLES environment +/// variable. The result is cached for the lifetime of the process. +/// +/// # Examples +/// +/// ```no_run +/// use uucore::hardware::simd_policy; +/// +/// let policy = simd_policy(); +/// if policy.allows_simd() { +/// println!("SIMD is enabled"); +/// } else { +/// println!("SIMD disabled by: {:?}", policy.disabled_features()); +/// } +/// ``` +pub fn simd_policy() -> &'static SimdPolicy { + static POLICY: OnceLock = OnceLock::new(); + POLICY.get_or_init(SimdPolicy::new) +} + +// Platform-specific feature detection + +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +fn detect_avx512() -> bool { + if cfg!(target_os = "android") { + false + } else { + std::arch::is_x86_feature_detected!("avx512f") + && std::arch::is_x86_feature_detected!("avx512bw") + } +} + +#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))] +fn detect_avx512() -> bool { + false +} + +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +fn detect_avx2() -> bool { + if cfg!(target_os = "android") { + false + } else { + std::arch::is_x86_feature_detected!("avx2") + } +} + +#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))] +fn detect_avx2() -> bool { + false +} + +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +fn detect_pclmul() -> bool { + if cfg!(target_os = "android") { + false + } else { + std::arch::is_x86_feature_detected!("pclmulqdq") + } +} + +#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))] +fn detect_pclmul() -> bool { + false +} + +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +fn detect_sse2() -> bool { + if cfg!(target_os = "android") { + false + } else { + std::arch::is_x86_feature_detected!("sse2") + } +} + +#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))] +fn detect_sse2() -> bool { + false +} + +#[cfg(all(target_arch = "aarch64", target_endian = "little"))] +fn detect_asimd() -> bool { + if cfg!(target_os = "android") { + false + } else { + std::arch::is_aarch64_feature_detected!("asimd") + } +} + +#[cfg(not(all(target_arch = "aarch64", target_endian = "little")))] +fn detect_asimd() -> bool { + false +} + +#[cfg(target_arch = "aarch64")] +fn detect_vmull() -> bool { + // VMULL is part of ARM NEON/ASIMD + // For now, we use ASIMD as a proxy + detect_asimd() +} + +#[cfg(not(target_arch = "aarch64"))] +fn detect_vmull() -> bool { + false +} + +// GLIBC_TUNABLES parsing + +/// Parse GLIBC_TUNABLES environment variable for disabled features +/// +/// Format: `glibc.cpu.hwcaps=-AVX2,-AVX512F` +/// Multiple tunable sections can be separated by colons. +fn parse_disabled_features(tunables: &str) -> Vec { + if tunables.is_empty() { + return Vec::new(); + } + + let mut disabled = Vec::new(); + + // GLIBC_TUNABLES format: "tunable1=value1:tunable2=value2" + for entry in tunables.split(':') { + let entry = entry.trim(); + let Some((name, raw_value)) = entry.split_once('=') else { + continue; + }; + + // We only care about glibc.cpu.hwcaps + if name.trim() != "glibc.cpu.hwcaps" { + continue; + } + + // Parse comma-separated features, disabled ones start with '-' + for token in raw_value.split(',') { + let token = token.trim(); + if let Some(feature) = token.strip_prefix('-') { + let feature = feature.trim().to_ascii_uppercase(); + if !feature.is_empty() { + disabled.push(feature); + } + } + } + } + + disabled +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_cpu_features_detection() { + let features = CpuFeatures::detect(); + // Just verify it doesn't panic and returns consistent results + let features2 = CpuFeatures::detect(); + assert_eq!(features, features2); + } + + #[test] + fn test_available_features() { + let features = CpuFeatures::detect(); + let available = features.available_features(); + // Should return a list (may be empty on some platforms) + assert!(available.iter().all(|s| !s.is_empty())); + } + + #[test] + fn test_parse_disabled_features_empty() { + assert_eq!(parse_disabled_features(""), Vec::::new()); + } + + #[test] + fn test_parse_disabled_features_single() { + let result = parse_disabled_features("glibc.cpu.hwcaps=-AVX2"); + assert_eq!(result, vec!["AVX2"]); + } + + #[test] + fn test_parse_disabled_features_multiple() { + let result = parse_disabled_features("glibc.cpu.hwcaps=-AVX2,-AVX512F"); + assert_eq!(result, vec!["AVX2", "AVX512F"]); + } + + #[test] + fn test_parse_disabled_features_mixed() { + let result = parse_disabled_features("glibc.cpu.hwcaps=-AVX2,SSE2,-AVX512F"); + // Only features with '-' prefix are disabled + assert_eq!(result, vec!["AVX2", "AVX512F"]); + } + + #[test] + fn test_parse_disabled_features_with_other_tunables() { + let result = + parse_disabled_features("glibc.malloc.check=1:glibc.cpu.hwcaps=-AVX2:other=value"); + assert_eq!(result, vec!["AVX2"]); + } + + #[test] + fn test_parse_disabled_features_case_insensitive() { + let result = parse_disabled_features("glibc.cpu.hwcaps=-avx2,-Avx512f"); + // Should normalize to uppercase + assert_eq!(result, vec!["AVX2", "AVX512F"]); + } + + #[test] + fn test_simd_policy() { + let policy = simd_policy(); + // Just verify it works + let _ = policy.allows_simd(); + let _ = policy.disabled_features(); + let _ = policy.enabled_features(); + } + + #[test] + fn test_simd_policy_caching() { + let policy1 = simd_policy(); + let policy2 = simd_policy(); + // Should be same instance (pointer equality) + assert!(std::ptr::eq(policy1, policy2)); + } +} diff --git a/src/uucore/src/lib/lib.rs b/src/uucore/src/lib/lib.rs index 000bd23fd..5459c5d54 100644 --- a/src/uucore/src/lib/lib.rs +++ b/src/uucore/src/lib/lib.rs @@ -54,6 +54,8 @@ pub use crate::features::fast_inc; pub use crate::features::format; #[cfg(feature = "fs")] pub use crate::features::fs; +#[cfg(feature = "hardware")] +pub use crate::features::hardware; #[cfg(feature = "i18n-common")] pub use crate::features::i18n; #[cfg(feature = "lines")] From d7dfafcdebe721a20b272f77833002535bf32a9d Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Tue, 25 Nov 2025 00:22:33 +0900 Subject: [PATCH 113/182] build-gnu.sh: Remove 2 sed hacks for tr --- util/build-gnu.sh | 6 ------ 1 file changed, 6 deletions(-) diff --git a/util/build-gnu.sh b/util/build-gnu.sh index 46a2852ef..4a36f803e 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -135,7 +135,6 @@ else "$([ "${SELINUX_ENABLED}" = 1 ] && echo --with-selinux || echo --without-selinux)" #Add timeout to to protect against hangs "${SED}" -i 's|^"\$@|'"${SYSTEM_TIMEOUT}"' 600 "\$@|' build-aux/test-driver - "${SED}" -i 's| tr | /usr/bin/tr |' tests/init.sh # Use a better diff "${SED}" -i 's|diff -c|diff -u|g' tests/Coreutils.pm "${MAKE}" -j "$("${NPROC}")" @@ -342,11 +341,6 @@ test \$n_stat1 -ge \$n_stat2 \\' tests/ls/stat-free-color.sh # Slightly different error message "${SED}" -i 's/not supported/unexpected argument/' tests/mv/mv-exchange.sh -# Most tests check that `/usr/bin/tr` is working correctly before running. -# However in NixOS/Nix-based distros, the tr util is located somewhere in -# /nix/store/xxxxxxxxxxxx...xxxx/bin/tr -# We just replace the references to `/usr/bin/tr` -"${SED}" -i 's/\/usr\/bin\/tr/$(command -v tr)/' tests/init.sh # upstream doesn't having the program name in the error message # but we do. We should keep it that way. From a16df34f9db42c7a6c2e6dab71c1e3ad91eb6dca Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Tue, 25 Nov 2025 15:51:29 +0900 Subject: [PATCH 114/182] Update Dockerfile: Don't apt-get jq (preinstalled) --- .devcontainer/Dockerfile | 1 - 1 file changed, 1 deletion(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 9befa73fa..4296d58c4 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -12,7 +12,6 @@ RUN apt-get update \ gcc \ gdb \ gperf \ - jq \ libacl1-dev \ libattr1-dev \ libcap-dev \ From b7e037ff9ff66e344216f85ebec195a456648013 Mon Sep 17 00:00:00 2001 From: Etienne Cordonnier Date: Tue, 25 Nov 2025 00:40:16 +0100 Subject: [PATCH 115/182] install: do not call chown when called as root - `pseudo` is a tool which simulates being root by intercepting calls to e.g. `geteuid` and `chown` (by using the `LD_PRELOAD` mechanism). This is used e.g. to build filesystems for embedded devices without running as root on the build machine. - the `chown` call getting removed in this commit does not work when running with `pseudo` and using `PSEUDO_IGNORE_PATHS`: in this case, the call to `geteuid()` gets intercepted by `libpseudo.so` and returns 0, however the call to `chown()` isn't intercepted by `libpseudo.so` in case it is in a path from `PSEUDO_IGNORE_PATHS`, and will thus fail since the process is not really root - the call to `chown()` was added in https://github.com/uutils/coreutils/pull/5735 with the intent of making the test `install-C-root.sh` pass, however it isn't required (GNU coreutils also does not call `chown` just because `install` was called as root) Fixes https://github.com/uutils/coreutils/issues/9116 Signed-off-by: Etienne Cordonnier --- src/uu/install/src/install.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/uu/install/src/install.rs b/src/uu/install/src/install.rs index 49252dcf9..ab05c7ca0 100644 --- a/src/uu/install/src/install.rs +++ b/src/uu/install/src/install.rs @@ -711,10 +711,9 @@ fn copy_files_into_dir(files: &[PathBuf], target_dir: &Path, b: &Behavior) -> UR Ok(()) } -/// Handle incomplete user/group parings for chown. +/// Handle ownership changes when -o/--owner or -g/--group flags are used. /// /// Returns a Result type with the Err variant containing the error message. -/// If the user is root, revert the uid & gid /// /// # Parameters /// @@ -735,11 +734,8 @@ fn chown_optional_user_group(path: &Path, b: &Behavior) -> UResult<()> { // Determine the owner and group IDs to be used for chown. let (owner_id, group_id) = if b.owner_id.is_some() || b.group_id.is_some() { (b.owner_id, b.group_id) - } else if geteuid() == 0 { - // Special case for root user. - (Some(0), Some(0)) } else { - // No chown operation needed. + // No chown operation needed - file ownership comes from process naturally. return Ok(()); }; From 824c5c7c937a9ff60e996c88584c89f77d65339f Mon Sep 17 00:00:00 2001 From: Chris Dryden Date: Wed, 26 Nov 2025 01:33:26 -0500 Subject: [PATCH 116/182] stty: Implemented saved state parser for stty (#9480) * Implemented saved state parser for stty * Add compatibility to macos flag type * Added many example state parsing integration tests with GNU compatibility checks and documentation * Spelling and formatting fixes * Matching behaviour of adding the help command after invocations and spelling fixes * GNU tests were being skipped because they were not at the sufficient version * Fixed messaging error for invalid states to not show full path * Normalizing the test output and reverting lib change * Discovered that the limit depends on platform specific values derived from a LIBC value * Spelling fixes and setting flags to 0 for cross platform compatibility * Clippy fixes * Disabling tests due to invalid printing of control chars and using GNU for printing * Redisabling failing test as outside of the scope of this PR * Adding g prefix support to normalize stderr * Spell checker fixes * Normalizing command for both gnu and uutils output * removing single value from testing since it can be interpreted as Baud rate * Fixing spelling mistake --- src/uu/stty/src/stty.rs | 93 +++++++++++++++++-- tests/by-util/test_stty.rs | 183 ++++++++++++++++++++++++++++++++++++- 2 files changed, 266 insertions(+), 10 deletions(-) diff --git a/src/uu/stty/src/stty.rs b/src/uu/stty/src/stty.rs index 42432c22c..8b8da5135 100644 --- a/src/uu/stty/src/stty.rs +++ b/src/uu/stty/src/stty.rs @@ -10,7 +10,7 @@ // spell-checker:ignore isig icanon iexten echoe crterase echok echonl noflsh xcase tostop echoprt prterase echoctl ctlecho echoke crtkill flusho extproc // spell-checker:ignore lnext rprnt susp swtch vdiscard veof veol verase vintr vkill vlnext vquit vreprint vstart vstop vsusp vswtc vwerase werase // spell-checker:ignore sigquit sigtstp -// spell-checker:ignore cbreak decctlq evenp litout oddp tcsadrain exta extb +// spell-checker:ignore cbreak decctlq evenp litout oddp tcsadrain exta extb NCCS mod flags; @@ -30,7 +30,7 @@ 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}; +use uucore::error::{UError, UResult, USimpleError, UUsageError}; use uucore::format_usage; use uucore::translate; @@ -150,6 +150,7 @@ enum ArgOptions<'a> { Mapping((S, u8)), Special(SpecialSetting), Print(PrintSetting), + SavedState(Vec), } impl<'a> From> for ArgOptions<'a> { @@ -352,8 +353,12 @@ fn stty(opts: &Options) -> UResult<()> { valid_args.push(ArgOptions::Print(PrintSetting::Size)); } _ => { + // Try to parse saved format (hex string like "6d02:5:4bf:8a3b:...") + if let Some(state) = parse_saved_state(arg) { + valid_args.push(ArgOptions::SavedState(state)); + } // control char - if let Some(char_index) = cc_to_index(arg) { + else if let Some(char_index) = cc_to_index(arg) { if let Some(mapping) = args_iter.next() { let cc_mapping = string_to_control_char(mapping).map_err(|e| { let message = match e { @@ -370,7 +375,7 @@ fn stty(opts: &Options) -> UResult<()> { ) } }; - USimpleError::new(1, message) + UUsageError::new(1, message) })?; valid_args.push(ArgOptions::Mapping((char_index, cc_mapping))); } else { @@ -418,6 +423,9 @@ fn stty(opts: &Options) -> UResult<()> { ArgOptions::Print(setting) => { print_special_setting(setting, opts.file.as_raw_fd())?; } + ArgOptions::SavedState(state) => { + apply_saved_state(&mut termios, state)?; + } } } tcsetattr(opts.file.as_fd(), set_arg, &termios)?; @@ -429,8 +437,9 @@ fn stty(opts: &Options) -> UResult<()> { Ok(()) } +// The GNU implementation adds the --help message when the args are incorrectly formatted fn missing_arg(arg: &str) -> Result> { - Err::>(USimpleError::new( + Err(UUsageError::new( 1, translate!( "stty-error-missing-argument", @@ -440,7 +449,7 @@ fn missing_arg(arg: &str) -> Result> { } fn invalid_arg(arg: &str) -> Result> { - Err::>(USimpleError::new( + Err(UUsageError::new( 1, translate!( "stty-error-invalid-argument", @@ -450,7 +459,7 @@ fn invalid_arg(arg: &str) -> Result> { } fn invalid_integer_arg(arg: &str) -> Result> { - Err::>(USimpleError::new( + Err(UUsageError::new( 1, translate!( "stty-error-invalid-integer-argument", @@ -478,6 +487,43 @@ fn parse_rows_cols(arg: &str) -> Option { None } +/// Parse a saved terminal state string in stty format. +/// +/// The format is colon-separated hexadecimal values: +/// `input_flags:output_flags:control_flags:local_flags:cc0:cc1:cc2:...` +/// +/// - Must have exactly 4 + NCCS parts (4 flags + platform-specific control characters) +/// - All parts must be non-empty valid hex values +/// - Control characters must fit in u8 (0-255) +/// - Returns `None` if format is invalid +fn parse_saved_state(arg: &str) -> Option> { + let parts: Vec<&str> = arg.split(':').collect(); + let expected_parts = 4 + nix::libc::NCCS; + + // GNU requires exactly the right number of parts for this platform + if parts.len() != expected_parts { + return None; + } + + // Validate all parts are non-empty valid hex + let mut values = Vec::with_capacity(expected_parts); + for (i, part) in parts.iter().enumerate() { + if part.is_empty() { + return None; // GNU rejects empty hex values + } + let val = u32::from_str_radix(part, 16).ok()?; + + // Control characters (indices 4+) must fit in u8 + if i >= 4 && val > 255 { + return None; + } + + values.push(val); + } + + Some(values) +} + fn check_flag_group(flag: &Flag, remove: bool) -> bool { remove && flag.group.is_some() } @@ -857,6 +903,39 @@ fn apply_char_mapping(termios: &mut Termios, mapping: &(S, u8)) { termios.control_chars[mapping.0 as usize] = mapping.1; } +/// Apply a saved terminal state to the current termios. +/// +/// The state array contains: +/// - `state[0]`: input flags +/// - `state[1]`: output flags +/// - `state[2]`: control flags +/// - `state[3]`: local flags +/// - `state[4..]`: control characters (optional) +/// +/// If state has fewer than 4 elements, no changes are applied. This is a defensive +/// check that should never trigger since `parse_saved_state` rejects such states. +fn apply_saved_state(termios: &mut Termios, state: &[u32]) -> nix::Result<()> { + // Require at least 4 elements for the flags (defensive check) + if state.len() < 4 { + return Ok(()); // No-op for invalid state (already validated by parser) + } + + // Apply the four flag groups, done (as _) for MacOS size compatibility + termios.input_flags = InputFlags::from_bits_truncate(state[0] as _); + termios.output_flags = OutputFlags::from_bits_truncate(state[1] as _); + termios.control_flags = ControlFlags::from_bits_truncate(state[2] as _); + termios.local_flags = LocalFlags::from_bits_truncate(state[3] as _); + + // Apply control characters if present (stored as u32 but used as u8) + for (i, &cc_val) in state.iter().skip(4).enumerate() { + if i < termios.control_chars.len() { + termios.control_chars[i] = cc_val as u8; + } + } + + Ok(()) +} + fn apply_special_setting( _termios: &mut Termios, setting: &SpecialSetting, diff --git a/tests/by-util/test_stty.rs b/tests/by-util/test_stty.rs index 9626c1406..f68de5daf 100644 --- a/tests/by-util/test_stty.rs +++ b/tests/by-util/test_stty.rs @@ -2,10 +2,18 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore parenb parmrk ixany iuclc onlcr icanon noflsh econl igpar ispeed ospeed +// spell-checker:ignore parenb parmrk ixany iuclc onlcr icanon noflsh econl igpar ispeed ospeed NCCS nonhex gstty -use uutests::new_ucmd; -use uutests::util::pty_path; +use uutests::util::{expected_result, pty_path}; +use uutests::{at_and_ts, new_ucmd, unwrap_or_return}; + +/// Normalize stderr by replacing the full binary path with just the utility name +/// This allows comparison between GNU (which shows "stty" or "gstty") and ours (which shows full path) +fn normalize_stderr(stderr: &str) -> String { + // Replace patterns like "Try 'gstty --help'" or "Try '/path/to/stty --help'" with "Try 'stty --help'" + let re = regex::Regex::new(r"Try '[^']*(?:g)?stty --help'").unwrap(); + re.replace_all(stderr, "Try 'stty --help'").to_string() +} #[test] fn test_invalid_arg() { @@ -349,3 +357,172 @@ fn non_negatable_combo() { .fails() .stderr_contains("invalid argument '-ek'"); } + +// Tests for saved state parsing and restoration +#[test] +#[cfg(unix)] +fn test_save_and_restore() { + let (path, _controller, _replica) = pty_path(); + let saved = new_ucmd!() + .args(&["--save", "--file", &path]) + .succeeds() + .stdout_move_str(); + + let saved = saved.trim(); + assert!(saved.contains(':')); + + new_ucmd!().args(&["--file", &path, saved]).succeeds(); +} + +#[test] +#[cfg(unix)] +fn test_save_with_g_flag() { + let (path, _controller, _replica) = pty_path(); + let saved = new_ucmd!() + .args(&["-g", "--file", &path]) + .succeeds() + .stdout_move_str(); + + let saved = saved.trim(); + assert!(saved.contains(':')); + + new_ucmd!().args(&["--file", &path, saved]).succeeds(); +} + +#[test] +#[cfg(unix)] +fn test_save_restore_after_change() { + let (path, _controller, _replica) = pty_path(); + let saved = new_ucmd!() + .args(&["--save", "--file", &path]) + .succeeds() + .stdout_move_str(); + + let saved = saved.trim(); + + new_ucmd!() + .args(&["--file", &path, "intr", "^A"]) + .succeeds(); + + new_ucmd!().args(&["--file", &path, saved]).succeeds(); + + new_ucmd!() + .args(&["--file", &path]) + .succeeds() + .stdout_str_check(|s| !s.contains("intr = ^A")); +} + +// These tests both validate what we expect each input to return and their error codes +// and also use the GNU coreutils results to validate our results match expectations +#[test] +#[cfg(unix)] +fn test_saved_state_valid_formats() { + let (path, _controller, _replica) = pty_path(); + let (_at, ts) = at_and_ts!(); + + // Generate valid saved state from the actual terminal + let saved = unwrap_or_return!(expected_result(&ts, &["-g", "--file", &path])).stdout_move_str(); + let saved = saved.trim(); + + let result = ts.ucmd().args(&["--file", &path, saved]).run(); + + result.success().no_stderr(); + + let exp_result = unwrap_or_return!(expected_result(&ts, &["--file", &path, saved])); + let normalized_stderr = normalize_stderr(result.stderr_str()); + result + .stdout_is(exp_result.stdout_str()) + .code_is(exp_result.code()); + assert_eq!(normalized_stderr, exp_result.stderr_str()); +} + +#[test] +#[cfg(unix)] +fn test_saved_state_invalid_formats() { + let (path, _controller, _replica) = pty_path(); + let (_at, ts) = at_and_ts!(); + + let num_cc = nix::libc::NCCS; + + // Build test strings with platform-specific counts + let cc_zeros = vec!["0"; num_cc].join(":"); + let cc_with_invalid = if num_cc > 0 { + let mut parts = vec!["1c"; num_cc]; + parts[0] = "100"; // First control char > 255 + parts.join(":") + } else { + String::new() + }; + let cc_with_space = if num_cc > 0 { + let mut parts = vec!["1c"; num_cc]; + parts[0] = "1c "; // Space in hex + parts.join(":") + } else { + String::new() + }; + let cc_with_nonhex = if num_cc > 0 { + let mut parts = vec!["1c"; num_cc]; + parts[0] = "xyz"; // Non-hex + parts.join(":") + } else { + String::new() + }; + let cc_with_empty = if num_cc > 0 { + let mut parts = vec!["1c"; num_cc]; + parts[0] = ""; // Empty + parts.join(":") + } else { + String::new() + }; + + // Cannot test single value since it would be interpreted as baud rate + let invalid_states = vec![ + "500:5:4bf".to_string(), // fewer than expected parts + "500:5:4bf:8a3b".to_string(), // only 4 parts + format!("500:5:{}:8a3b:{}", cc_zeros, "extra"), // too many parts + format!("500::4bf:8a3b:{}", cc_zeros), // empty hex value in flags + format!("500:5:4bf:8a3b:{}", cc_with_empty), // empty hex value in cc + format!("500:5:4bf:8a3b:{}", cc_with_nonhex), // non-hex characters + format!("500:5:4bf:8a3b:{}", cc_with_space), // space in hex value + format!("500:5:4bf:8a3b:{}", cc_with_invalid), // control char > 255 + ]; + + for state in &invalid_states { + let result = ts.ucmd().args(&["--file", &path, state]).run(); + + result.failure().stderr_contains("invalid argument"); + + let exp_result = unwrap_or_return!(expected_result(&ts, &["--file", &path, state])); + let normalized_stderr = normalize_stderr(result.stderr_str()); + let exp_normalized_stderr = normalize_stderr(exp_result.stderr_str()); + result + .stdout_is(exp_result.stdout_str()) + .code_is(exp_result.code()); + assert_eq!(normalized_stderr, exp_normalized_stderr); + } +} + +#[test] +#[cfg(unix)] +#[ignore = "Fails because the implementation of print state is not correctly printing flags on certain platforms"] +fn test_saved_state_with_control_chars() { + let (path, _controller, _replica) = pty_path(); + let (_at, ts) = at_and_ts!(); + + // Build a valid saved state with platform-specific number of control characters + let num_cc = nix::libc::NCCS; + let cc_values: Vec = (1..=num_cc).map(|_| format!("{:x}", 0)).collect(); + let saved_state = format!("500:5:4bf:8a3b:{}", cc_values.join(":")); + + ts.ucmd().args(&["--file", &path, &saved_state]).succeeds(); + + let result = ts.ucmd().args(&["-g", "--file", &path]).run(); + + result.success().stdout_contains(":"); + + let exp_result = unwrap_or_return!(expected_result(&ts, &["-g", "--file", &path])); + result + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str()) + .code_is(exp_result.code()); +} From 5fd26c067190b44ebf59bdbc1254dad6160fcbdf Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Wed, 26 Nov 2025 18:15:04 +0900 Subject: [PATCH 117/182] build-gnu.sh: Reduce time to build GNU coreutils (#9475) --- util/build-gnu.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/util/build-gnu.sh b/util/build-gnu.sh index 4a36f803e..91bbc114f 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -4,7 +4,7 @@ # spell-checker:ignore (paths) abmon deref discrim eacces getlimits getopt ginstall inacc infloop inotify reflink ; (misc) INT_OFLOW OFLOW # spell-checker:ignore baddecode submodules xstrtol distros ; (vars/env) SRCDIR vdir rcexp xpart dired OSTYPE ; (utils) gnproc greadlink gsed multihardlink texinfo CARGOFLAGS -# spell-checker:ignore openat TOCTOU +# spell-checker:ignore openat TOCTOU CFLAGS set -e @@ -131,7 +131,8 @@ else # Disable useless checks "${SED}" -i 's|check-texinfo: $(syntax_checks)|check-texinfo:|' doc/local.mk ./bootstrap --skip-po - ./configure --quiet --disable-gcc-warnings --disable-nls --disable-dependency-tracking --disable-bold-man-page-references \ + # Use CFLAGS for best build time since we discard GNU coreutils + CFLAGS="${CFLAGS} -pipe -O0 -s" ./configure --quiet --disable-gcc-warnings --disable-nls --disable-dependency-tracking --disable-bold-man-page-references \ "$([ "${SELINUX_ENABLED}" = 1 ] && echo --with-selinux || echo --without-selinux)" #Add timeout to to protect against hangs "${SED}" -i 's|^"\$@|'"${SYSTEM_TIMEOUT}"' 600 "\$@|' build-aux/test-driver From d655eed48937063dc7cd64391bf64b3762e71cc4 Mon Sep 17 00:00:00 2001 From: naoNao89 <90588855+naoNao89@users.noreply.github.com> Date: Tue, 25 Nov 2025 04:59:59 +0700 Subject: [PATCH 118/182] feat(cksum): improve debug output for single file operations --- src/uu/cksum/Cargo.toml | 7 +- src/uu/cksum/locales/en-US.ftl | 1 + src/uu/cksum/locales/fr-FR.ftl | 1 + src/uu/cksum/src/cksum.rs | 37 +++++++++++ tests/by-util/test_cksum.rs | 115 ++++++++++++++++++++++++++++++++- 5 files changed, 157 insertions(+), 4 deletions(-) diff --git a/src/uu/cksum/Cargo.toml b/src/uu/cksum/Cargo.toml index 01ca5cb16..7e62c5c8f 100644 --- a/src/uu/cksum/Cargo.toml +++ b/src/uu/cksum/Cargo.toml @@ -19,7 +19,12 @@ path = "src/cksum.rs" [dependencies] clap = { workspace = true } -uucore = { workspace = true, features = ["checksum", "encoding", "sum"] } +uucore = { workspace = true, features = [ + "checksum", + "encoding", + "sum", + "hardware", +] } hex = { workspace = true } fluent = { workspace = true } diff --git a/src/uu/cksum/locales/en-US.ftl b/src/uu/cksum/locales/en-US.ftl index 0506d1bbe..4a49caebd 100644 --- a/src/uu/cksum/locales/en-US.ftl +++ b/src/uu/cksum/locales/en-US.ftl @@ -27,6 +27,7 @@ cksum-help-status = don't output anything, status code shows success cksum-help-quiet = don't print OK for each successfully verified file cksum-help-ignore-missing = don't fail or report status for missing files cksum-help-zero = end each output line with NUL, not newline, and disable file name escaping +cksum-help-debug = print CPU hardware capability detection info used by cksum # Error messages cksum-error-is-directory = { $file }: Is a directory diff --git a/src/uu/cksum/locales/fr-FR.ftl b/src/uu/cksum/locales/fr-FR.ftl index 1a045dddb..686584696 100644 --- a/src/uu/cksum/locales/fr-FR.ftl +++ b/src/uu/cksum/locales/fr-FR.ftl @@ -27,6 +27,7 @@ cksum-help-status = ne rien afficher, le code de statut indique le succès cksum-help-quiet = ne pas afficher OK pour chaque fichier vérifié avec succès cksum-help-ignore-missing = ne pas échouer ou signaler le statut pour les fichiers manquants cksum-help-zero = terminer chaque ligne de sortie avec NUL, pas un saut de ligne, et désactiver l'échappement des noms de fichiers +cksum-help-debug = afficher les informations de débogage sur la détection de la prise en charge matérielle du processeur # Messages d'erreur cksum-error-is-directory = { $file } : Est un répertoire diff --git a/src/uu/cksum/src/cksum.rs b/src/uu/cksum/src/cksum.rs index 499fc52c0..dd75dcdee 100644 --- a/src/uu/cksum/src/cksum.rs +++ b/src/uu/cksum/src/cksum.rs @@ -20,9 +20,34 @@ use uucore::checksum::{ sanitize_sha2_sha3_length_str, }; use uucore::error::UResult; +use uucore::hardware::CpuFeatures; use uucore::line_ending::LineEnding; use uucore::{format_usage, translate}; +/// Print CPU hardware capability detection information to stderr +/// This matches GNU cksum's --debug behavior +fn print_cpu_debug_info() { + let features = CpuFeatures::detect(); + + fn print_feature(name: &str, available: bool) { + if available { + eprintln!("cksum: using {name} hardware support"); + } else { + eprintln!("cksum: {name} support not detected"); + } + } + + // x86/x86_64 + print_feature("avx512", features.has_avx512()); + print_feature("avx2", features.has_avx2()); + print_feature("pclmul", features.has_pclmul()); + + // ARM aarch64 + if cfg!(target_arch = "aarch64") { + print_feature("vmull", features.has_vmull()); + } +} + mod options { pub const ALGORITHM: &str = "algorithm"; pub const FILE: &str = "file"; @@ -40,6 +65,7 @@ mod options { pub const IGNORE_MISSING: &str = "ignore-missing"; pub const QUIET: &str = "quiet"; pub const ZERO: &str = "zero"; + pub const DEBUG: &str = "debug"; } /// cksum has a bunch of legacy behavior. We handle this in this function to @@ -181,6 +207,11 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { matches.get_flag(options::BASE64), ); + // Print hardware debug info if requested + if matches.get_flag(options::DEBUG) { + print_cpu_debug_info(); + } + let opts = ChecksumComputeOptions { algo_kind: algo, output_format, @@ -317,5 +348,11 @@ pub fn uu_app() -> Command { .help(translate!("cksum-help-zero")) .action(ArgAction::SetTrue), ) + .arg( + Arg::new(options::DEBUG) + .long(options::DEBUG) + .help(translate!("cksum-help-debug")) + .action(ArgAction::SetTrue), + ) .after_help(translate!("cksum-after-help")) } diff --git a/tests/by-util/test_cksum.rs b/tests/by-util/test_cksum.rs index 57f11b8ef..3d707eb78 100644 --- a/tests/by-util/test_cksum.rs +++ b/tests/by-util/test_cksum.rs @@ -10,9 +10,8 @@ use uutests::util::TestScenario; use uutests::util::log_info; use uutests::util_name; -const ALGOS: [&str; 12] = [ - "sysv", "bsd", "crc", "crc32b", "md5", "sha1", "sha224", "sha256", "sha384", "sha512", - "blake2b", "sm3", +const ALGOS: [&str; 11] = [ + "sysv", "bsd", "crc", "md5", "sha1", "sha224", "sha256", "sha384", "sha512", "blake2b", "sm3", ]; const SHA_LENGTHS: [u32; 4] = [224, 256, 384, 512]; @@ -2876,3 +2875,113 @@ mod format_mix { .stderr_contains("cksum: WARNING: 1 line is improperly formatted"); } } + +#[cfg(not(target_os = "android"))] +mod debug_flag { + use super::*; + + #[test] + fn test_debug_flag() { + // Test with default CRC algorithm - should output CPU feature detection + new_ucmd!() + .arg("--debug") + .arg("lorem_ipsum.txt") + .succeeds() + .stdout_is_fixture("crc_single_file.expected") + .stderr_contains("avx512") + .stderr_contains("avx2") + .stderr_contains("pclmul"); + + // Test with MD5 algorithm - CPU detection should be same regardless of algorithm + new_ucmd!() + .arg("--debug") + .arg("-a") + .arg("md5") + .arg("lorem_ipsum.txt") + .succeeds() + .stdout_is_fixture("md5_single_file.expected") + .stderr_contains("avx512") + .stderr_contains("avx2") + .stderr_contains("pclmul"); + + // Test with stdin - CPU detection should appear once + new_ucmd!() + .arg("--debug") + .pipe_in("test") + .succeeds() + .stderr_contains("avx512") + .stderr_contains("avx2") + .stderr_contains("pclmul"); + + // Test with multiple files - CPU detection should appear once, not per file + new_ucmd!() + .arg("--debug") + .arg("lorem_ipsum.txt") + .arg("alice_in_wonderland.txt") + .succeeds() + .stdout_is_fixture("crc_multiple_files.expected") + .stderr_str_check(|stderr| { + // Verify CPU detection happens only once by checking the count of each feature line + let avx512_count = stderr + .lines() + .filter(|line| line.contains("avx512")) + .count(); + let avx2_count = stderr.lines().filter(|line| line.contains("avx2")).count(); + let pclmul_count = stderr + .lines() + .filter(|line| line.contains("pclmul")) + .count(); + + avx512_count == 1 && avx2_count == 1 && pclmul_count == 1 + }); + } + + #[test] + fn test_debug_with_algorithms() { + // Test with SHA256 - CPU detection should be same regardless of algorithm + new_ucmd!() + .arg("--debug") + .arg("-a") + .arg("sha256") + .arg("lorem_ipsum.txt") + .succeeds() + .stderr_contains("avx512") + .stderr_contains("avx2") + .stderr_contains("pclmul"); + + // Test with BLAKE2b default length + new_ucmd!() + .arg("--debug") + .arg("-a") + .arg("blake2b") + .arg("lorem_ipsum.txt") + .succeeds() + .stderr_contains("avx512") + .stderr_contains("avx2") + .stderr_contains("pclmul"); + + // Test with BLAKE2b custom length + new_ucmd!() + .arg("--debug") + .arg("-a") + .arg("blake2b") + .arg("--length") + .arg("256") + .arg("lorem_ipsum.txt") + .succeeds() + .stderr_contains("avx512") + .stderr_contains("avx2") + .stderr_contains("pclmul"); + + // Test with SHA1 + new_ucmd!() + .arg("--debug") + .arg("-a") + .arg("sha1") + .arg("lorem_ipsum.txt") + .succeeds() + .stderr_contains("avx512") + .stderr_contains("avx2") + .stderr_contains("pclmul"); + } +} From 9e2fec6678d906fb70b3fb00ae3659f1dd19397b Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Thu, 27 Nov 2025 06:54:51 +0900 Subject: [PATCH 119/182] CICD.yml: Stop publishing conflicting artifacts (#9491) --- .github/workflows/CICD.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index 93f1fac79..b0992cd2b 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -579,14 +579,14 @@ jobs: # - { 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 } - - { os: ubuntu-latest , target: x86_64-unknown-linux-gnu , features: "feat_os_unix,test_risky_names", 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 } - { os: ubuntu-latest , target: x86_64-unknown-redox , features: feat_os_unix_redox , use-cross: redoxer , skip-tests: true } - { os: ubuntu-latest , target: wasm32-unknown-unknown , default-features: false, features: uucore/format, skip-tests: true, skip-package: true, skip-publish: true } - { os: macos-latest , target: aarch64-apple-darwin , features: feat_os_macos, workspace-tests: true } # M1 CPU - # PR #7964: Mac should still build even if the feature is not enabled - - { os: macos-latest , target: aarch64-apple-darwin , workspace-tests: true } # M1 CPU + # PR #7964: Mac should still build even if the feature is not enabled. Do not publish this. + - { os: macos-latest , target: aarch64-apple-darwin , workspace-tests: true, skip-publish: true } # M1 CPU - { os: macos-latest , target: x86_64-apple-darwin , features: feat_os_macos, workspace-tests: true } - { os: windows-latest , target: i686-pc-windows-msvc , features: feat_os_windows } - { os: windows-latest , target: x86_64-pc-windows-gnu , features: feat_os_windows } From df959b7e00763c874813777409261bfedfdf75d4 Mon Sep 17 00:00:00 2001 From: Vikram Kangotra <61800198+vikram-kangotra@users.noreply.github.com> Date: Thu, 27 Nov 2025 03:45:04 +0530 Subject: [PATCH 120/182] Merge pull request #9410 from vikram-kangotra/fix/ls-proc-self-fd-regression ls: prevent ReadDir from closing before entries are processed --- src/uu/ls/src/ls.rs | 4 ++-- tests/by-util/test_ls.rs | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 6f038142a..e66da6b6e 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -2305,7 +2305,7 @@ fn should_display(entry: &DirEntry, config: &Config) -> bool { #[allow(clippy::cognitive_complexity)] fn enter_directory( path_data: &PathData, - read_dir: ReadDir, + mut read_dir: ReadDir, config: &Config, state: &mut ListState, listed_ancestors: &mut HashSet, @@ -2334,7 +2334,7 @@ fn enter_directory( }; // Convert those entries to the PathData struct - for raw_entry in read_dir { + for raw_entry in read_dir.by_ref() { let dir_entry = match raw_entry { Ok(path) => path, Err(err) => { diff --git a/tests/by-util/test_ls.rs b/tests/by-util/test_ls.rs index ef7591b8a..38729d306 100644 --- a/tests/by-util/test_ls.rs +++ b/tests/by-util/test_ls.rs @@ -6663,3 +6663,18 @@ fn test_f_with_long_format() { // Long format should still work (contains permissions, etc.) assert!(result.contains("-rw")); } + +#[test] +#[cfg(target_os = "linux")] +fn test_ls_proc_self_fd_no_errors() { + // Regression test: ReadDir must stay alive until metadata() is called + // to prevent "cannot access '/proc/self/fd/3'" errors. + let scene = TestScenario::new(util_name!()); + + scene + .ucmd() + .arg("-l") + .arg("/proc/self/fd") + .succeeds() + .stderr_does_not_contain("cannot access"); +} From b9f97d4c7de6a898dee908f73221af2e5d1896e2 Mon Sep 17 00:00:00 2001 From: mattsu <35655889+mattsu2020@users.noreply.github.com> Date: Thu, 27 Nov 2025 07:20:45 +0900 Subject: [PATCH 121/182] fix(seq): handle BrokenPipe like GNU (#9471) * fix(seq): handle BrokenPipe like GNU * test: add Unix-specific test for seq command broken pipe handling - Ensures seq exits gracefully with code 0 and reports "Broken pipe" error on stderr when stdout pipe is prematurely closed - Validates correct behavior for common scenario where output is piped to commands like head that terminate early * refactor(test): translate Japanese comment to English in test_seq.rs - Updated a comment in the test for broken pipe behavior to use English instead of Japanese, enhancing readability for non-Japanese speakers and aligning with project standards. No functional changes to the test logic. --- src/uu/seq/src/seq.rs | 9 +++++++-- tests/by-util/test_seq.rs | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/uu/seq/src/seq.rs b/src/uu/seq/src/seq.rs index 674135660..7b56c26f5 100644 --- a/src/uu/seq/src/seq.rs +++ b/src/uu/seq/src/seq.rs @@ -4,7 +4,7 @@ // file that was distributed with this source code. // spell-checker:ignore (ToDO) bigdecimal extendedbigdecimal numberparse hexadecimalfloat biguint use std::ffi::{OsStr, OsString}; -use std::io::{BufWriter, ErrorKind, Write, stdout}; +use std::io::{BufWriter, Write, stdout}; use clap::{Arg, ArgAction, Command}; use num_bigint::BigUint; @@ -211,7 +211,12 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { match result { Ok(()) => Ok(()), - Err(err) if err.kind() == ErrorKind::BrokenPipe => Ok(()), + Err(err) if err.kind() == std::io::ErrorKind::BrokenPipe => { + // GNU seq prints the Broken pipe message but still exits with status 0 + let err = err.map_err_context(|| "write error".into()); + uucore::show_error!("{err}"); + Ok(()) + } Err(err) => Err(err.map_err_context(|| "write error".into())), } } diff --git a/tests/by-util/test_seq.rs b/tests/by-util/test_seq.rs index f82a6228f..de0ad10d9 100644 --- a/tests/by-util/test_seq.rs +++ b/tests/by-util/test_seq.rs @@ -10,6 +10,25 @@ fn test_invalid_arg() { new_ucmd!().arg("--definitely-invalid").fails_with_code(1); } +#[test] +#[cfg(unix)] +fn test_broken_pipe_still_exits_success() { + use std::process::Stdio; + + let mut child = new_ucmd!() + .args(&["1", "5"]) + .set_stdout(Stdio::piped()) + .run_no_wait(); + + // Trigger a Broken pipe by writing to a pipe whose reader closed first. + child.close_stdout(); + let result = child.wait().unwrap(); + + result + .code_is(0) + .stderr_contains("write error: Broken pipe"); +} + #[test] fn test_no_args() { new_ucmd!() From 2e65099999ffbe014d793c8800cf6b7d7c8cf54d Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Thu, 27 Nov 2025 21:01:04 +0900 Subject: [PATCH 122/182] CICD.yml: Removed unused code for i586 --- .github/workflows/CICD.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index b0992cd2b..04917d9ef 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -637,7 +637,6 @@ jobs: case '${{ matrix.job.target }}' in aarch64-*) TARGET_ARCH=arm64 ;; arm-*-*hf) TARGET_ARCH=armhf ;; - i586-*) TARGET_ARCH=i586 ;; i686-*) TARGET_ARCH=i686 ;; x86_64-*) TARGET_ARCH=x86_64 ;; esac; From b8da17d925e25d36cf30afd456af3cb70a2aa357 Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Thu, 27 Nov 2025 20:37:14 +0100 Subject: [PATCH 123/182] env: remove outdated comment (#9496) --- src/uu/env/src/env.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/uu/env/src/env.rs b/src/uu/env/src/env.rs index fbd233105..da0daf80c 100644 --- a/src/uu/env/src/env.rs +++ b/src/uu/env/src/env.rs @@ -102,7 +102,6 @@ struct Options<'a> { } /// print `name=value` env pairs on screen -/// if null is true, separate pairs with a \0, \n otherwise fn print_env(line_ending: LineEnding) { let stdout_raw = io::stdout(); let mut stdout = stdout_raw.lock(); From f6d581fc48027528a89e58236a66475aca4e3c80 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Fri, 28 Nov 2025 04:38:07 +0900 Subject: [PATCH 124/182] build-gnu.sh: Remove hfs dep from hardlink-case.sh (#9482) Co-authored-by: Sylvestre Ledru --- util/build-gnu.sh | 4 ++++ util/why-skip.md | 3 --- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/util/build-gnu.sh b/util/build-gnu.sh index 91bbc114f..c5bf37267 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -5,6 +5,7 @@ # spell-checker:ignore (paths) abmon deref discrim eacces getlimits getopt ginstall inacc infloop inotify reflink ; (misc) INT_OFLOW OFLOW # spell-checker:ignore baddecode submodules xstrtol distros ; (vars/env) SRCDIR vdir rcexp xpart dired OSTYPE ; (utils) gnproc greadlink gsed multihardlink texinfo CARGOFLAGS # spell-checker:ignore openat TOCTOU CFLAGS +# spell-checker:ignore hfsplus casefold chattr set -e @@ -167,6 +168,9 @@ grep -rl 'path_prepend_' tests/* | xargs -r "${SED}" -i 's| path_prepend_ ./src| # path_prepend_ sets $abs_path_dir_: set it manually instead. grep -rl '\$abs_path_dir_' tests/*/*.sh | xargs -r "${SED}" -i "s|\$abs_path_dir_|${UU_BUILD_DIR//\//\\/}|g" +# Remove hfs dependency (should be merged to upstream) +"${SED}" -i -e "s|hfsplus|ext4 -O casefold|" -e "s|cd mnt|rm -d mnt/lost+found;chattr +F mnt;cd mnt|" tests/mv/hardlink-case.sh + # Use the system coreutils where the test fails due to error in a util that is not the one being tested "${SED}" -i "s|grep '^#define HAVE_CAP 1' \$CONFIG_HEADER > /dev/null|true|" tests/ls/capability.sh diff --git a/util/why-skip.md b/util/why-skip.md index 915b9460e..19310a71e 100644 --- a/util/why-skip.md +++ b/util/why-skip.md @@ -13,9 +13,6 @@ = LD_PRELOAD was ineffective? = * tests/cp/nfs-removal-race.sh -= failed to create hfs file system = -* tests/mv/hardlink-case.sh - = temporarily disabled = * tests/mkdir/writable-under-readonly.sh From 43dd238feae62428819edf1ef0db7ec57675f90a Mon Sep 17 00:00:00 2001 From: mattsu <35655889+mattsu2020@users.noreply.github.com> Date: Fri, 28 Nov 2025 16:23:55 +0900 Subject: [PATCH 125/182] od: make GNU test od.pl pass (#9334) * feat: Add support for long double floating-point numbers and refine general float formatting. * feat: Enhance `od` error reporting for file I/O, width, and offset parsing, including overflow detection and input validation. * feat: Improve long double parsing by converting f128 to f64, enhance overflow error reporting with `libc::ERANGE`, and prevent final offset printing on input errors. * style: Apply minor formatting adjustments across the `od` module. * refactor: simplify float formatting logic and update string handling syntax * fix: Correct float formatting logic to use decimal for numbers within range and exponential otherwise. * refactor(test): use helper function in test_calculate_alignment Replace repetitive assert_eq! calls with a new assert_alignment helper to improve test readability and reduce code duplication. The helper encapsulates alignment checks for OutputInfo::calculate_alignment, making tests clearer and easier to maintain. * feat(cspell): add ERANGE to jargon wordlist Added "ERANGE" to the dictionary to prevent spell checker flagging it as a misspelling, as it's a valid errno constant from C libraries. * feat(od): improve width error handling and subnormal float output Refactor width option parsing in OdOptions to use i18n-compatible error messages via translate! macro, consolidating redundant error branches for better maintainability. Enhance float formatting for f16 and bf16 by introducing format_binary16_like helper to properly display subnormal values with exponential notation, removing the obsolete format_float_simple function and adding subnormal detection functions for accurate representation in od's output. * refactor(od): simplify format_item_bf16 by removing redundant variable Remove unnecessary `value` variable in `format_item_bf16` function, eliminating a redundant cast and inline `f` directly for clarity and minor efficiency gain. * fix(od): standardize option names in error messages Remove hardcoded "--" prefixes from localization strings in en-US.ftl and fr-FR.ftl, replacing with a computed display name that includes "--" and optionally the short form (e.g., "--option" or "--option, -s"). Update parse_bytes_option and read_bytes functions to pass an option_display_name, enabling consistent error message formatting across localizations. Add validation to reject zero width values as invalid arguments. Improves user experience by providing clearer, more consistent option references in error outputs. * refactor: condense format! macro in format_item_bf16 for readability Removed unnecessary line breaks in the format! expression, keeping the code more concise while maintaining functionality. This improves code style in the float printing module. * fix(od): add external quoting for filenames in error messages The MultifileReader now uses `fname.maybe_quote().external(true)` when displaying permission and I/O errors, ensuring filenames are properly quoted for user-facing output (e.g., handling special characters that might confuse shells). This prevents potential issues with filename display in error logs. * refactor(od): Rename f128_to_f64 to u128_to_f64 for clarity Renamed the function in input_decoder.rs from f128_to_f64 to u128_to_f64 to accurately reflect its purpose of converting u128 integer bits to f64, improving code readability and reducing potential confusion over float types. * refactor(od): simplify error handling in OdOptions using combinators Use map_err and the try operator to replace a verbose match statement, making the code more concise and idiomatic Rust. This improves readability without altering functionality. * Update src/uu/od/src/od.rs Co-authored-by: Daniel Hofstetter * Update src/uu/od/src/od.rs Co-authored-by: Daniel Hofstetter * Update src/uu/od/src/parse_inputs.rs Co-authored-by: Daniel Hofstetter * Update src/uu/od/src/od.rs Co-authored-by: Daniel Hofstetter * Update src/uu/od/src/parse_inputs.rs Co-authored-by: Daniel Hofstetter * Update src/uu/od/src/parse_inputs.rs Co-authored-by: Daniel Hofstetter * refactor(od): remove leaking from translated error messages in parse_offset_operand Eliminated use of `.leak()` and unnecessary `.to_string()` calls on translated error strings in the `parse_offset_operand` function. This simplifies error handling, improves memory safety by avoiding intentional leaks, and makes the code cleaner without functional changes. --------- Co-authored-by: Daniel Hofstetter --- .../cspell.dictionaries/jargon.wordlist.txt | 1 + Cargo.lock | 1 + src/uu/od/Cargo.toml | 1 + src/uu/od/locales/en-US.ftl | 7 +- src/uu/od/locales/fr-FR.ftl | 6 +- src/uu/od/src/byteorder_io.rs | 3 +- src/uu/od/src/formatter_item_info.rs | 5 + src/uu/od/src/input_decoder.rs | 57 ++- src/uu/od/src/multifile_reader.rs | 8 +- src/uu/od/src/od.rs | 80 +++- src/uu/od/src/output_info.rs | 392 +++++++++--------- src/uu/od/src/parse_formats.rs | 6 +- src/uu/od/src/parse_inputs.rs | 99 ++++- src/uu/od/src/prn_float.rs | 63 ++- tests/by-util/test_od.rs | 212 +++++++++- 15 files changed, 675 insertions(+), 266 deletions(-) diff --git a/.vscode/cspell.dictionaries/jargon.wordlist.txt b/.vscode/cspell.dictionaries/jargon.wordlist.txt index 0806d14ba..a757953b4 100644 --- a/.vscode/cspell.dictionaries/jargon.wordlist.txt +++ b/.vscode/cspell.dictionaries/jargon.wordlist.txt @@ -41,6 +41,7 @@ duplicative dsync endianness enqueue +ERANGE errored executable executables diff --git a/Cargo.lock b/Cargo.lock index fa7a7d13f..72b1ca0d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3639,6 +3639,7 @@ dependencies = [ "clap", "fluent", "half", + "libc", "uucore", ] diff --git a/src/uu/od/Cargo.toml b/src/uu/od/Cargo.toml index a2cd59a63..97676a86e 100644 --- a/src/uu/od/Cargo.toml +++ b/src/uu/od/Cargo.toml @@ -23,6 +23,7 @@ clap = { workspace = true } half = { workspace = true } uucore = { workspace = true, features = ["parser"] } fluent = { workspace = true } +libc.workspace = true [[bin]] name = "od" diff --git a/src/uu/od/locales/en-US.ftl b/src/uu/od/locales/en-US.ftl index 208bd3333..bcafe1fe2 100644 --- a/src/uu/od/locales/en-US.ftl +++ b/src/uu/od/locales/en-US.ftl @@ -55,9 +55,10 @@ od-error-invalid-offset = invalid offset: {$offset} od-error-invalid-label = invalid label: {$label} od-error-too-many-inputs = too many inputs after --traditional: {$input} od-error-parse-failed = parse failed -od-error-invalid-suffix = invalid suffix in --{$option} argument {$value} -od-error-invalid-argument = invalid --{$option} argument {$value} -od-error-argument-too-large = --{$option} argument {$value} too large +od-error-overflow = Numerical result out of range +od-error-invalid-suffix = invalid suffix in {$option} argument {$value} +od-error-invalid-argument = invalid {$option} argument {$value} +od-error-argument-too-large = {$option} argument {$value} too large od-error-skip-past-end = tried to skip past end of input # Help messages diff --git a/src/uu/od/locales/fr-FR.ftl b/src/uu/od/locales/fr-FR.ftl index cba433b64..df07eebe6 100644 --- a/src/uu/od/locales/fr-FR.ftl +++ b/src/uu/od/locales/fr-FR.ftl @@ -56,9 +56,9 @@ od-error-invalid-offset = décalage invalide : {$offset} od-error-invalid-label = étiquette invalide : {$label} od-error-too-many-inputs = trop d'entrées après --traditional : {$input} od-error-parse-failed = échec de l'analyse -od-error-invalid-suffix = suffixe invalide dans l'argument --{$option} {$value} -od-error-invalid-argument = argument --{$option} invalide {$value} -od-error-argument-too-large = argument --{$option} {$value} trop grand +od-error-invalid-suffix = suffixe invalide dans l'argument {$option} {$value} +od-error-invalid-argument = argument {$option} invalide {$value} +od-error-argument-too-large = argument {$option} {$value} trop grand od-error-skip-past-end = tentative d'ignorer au-delà de la fin de l'entrée # Messages d'aide diff --git a/src/uu/od/src/byteorder_io.rs b/src/uu/od/src/byteorder_io.rs index 545016ff3..8cc7a8bac 100644 --- a/src/uu/od/src/byteorder_io.rs +++ b/src/uu/od/src/byteorder_io.rs @@ -52,5 +52,6 @@ gen_byte_order_ops! { read_i32, write_i32 -> i32, read_i64, write_i64 -> i64, read_f32, write_f32 -> f32, - read_f64, write_f64 -> f64 + read_f64, write_f64 -> f64, + read_u128, write_u128 -> u128 } diff --git a/src/uu/od/src/formatter_item_info.rs b/src/uu/od/src/formatter_item_info.rs index e530a0a3e..472c9fc4e 100644 --- a/src/uu/od/src/formatter_item_info.rs +++ b/src/uu/od/src/formatter_item_info.rs @@ -12,6 +12,7 @@ use std::fmt; pub enum FormatWriter { IntWriter(fn(u64) -> String), FloatWriter(fn(f64) -> String), + LongDoubleWriter(fn(f64) -> String), // On most platforms, long double is f64 or emulated BFloatWriter(fn(f64) -> String), MultibyteWriter(fn(&[u8]) -> String), } @@ -27,6 +28,10 @@ impl fmt::Debug for FormatWriter { f.write_str("FloatWriter:")?; fmt::Pointer::fmt(p, f) } + Self::LongDoubleWriter(ref p) => { + f.write_str("LongDoubleWriter:")?; + fmt::Pointer::fmt(p, f) + } Self::BFloatWriter(ref p) => { f.write_str("BFloatWriter:")?; fmt::Pointer::fmt(p, f) diff --git a/src/uu/od/src/input_decoder.rs b/src/uu/od/src/input_decoder.rs index a65e7613b..416badb44 100644 --- a/src/uu/od/src/input_decoder.rs +++ b/src/uu/od/src/input_decoder.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 bfloat multifile +// spell-checker:ignore bfloat multifile mant use half::{bf16, f16}; use std::io; @@ -165,6 +165,61 @@ impl MemoryDecoder<'_> { let val = f32::from(bf16::from_bits(bits)); f64::from(val) } + + /// Returns a long double from the internal buffer at position `start`. + /// We read 16 bytes as u128 (respecting endianness) and convert to f64. + /// This ensures that endianness swapping works correctly even if we lose precision. + pub fn read_long_double(&self, start: usize) -> f64 { + let bits = self.byte_order.read_u128(&self.data[start..start + 16]); + u128_to_f64(bits) + } +} + +fn u128_to_f64(u: u128) -> f64 { + let sign = (u >> 127) as u64; + let exp = ((u >> 112) & 0x7FFF) as u64; + let mant = u & ((1 << 112) - 1); + + if exp == 0x7FFF { + // Infinity or NaN + if mant == 0 { + if sign == 0 { + f64::INFINITY + } else { + f64::NEG_INFINITY + } + } else { + f64::NAN + } + } else if exp == 0 { + // Subnormal or zero + if mant == 0 { + if sign == 0 { 0.0 } else { -0.0 } + } else { + // Subnormal f128 is too small for f64, flush to zero + if sign == 0 { 0.0 } else { -0.0 } + } + } else { + // Normal + let new_exp = exp as i64 - 16383 + 1023; + if new_exp >= 2047 { + // Overflow to infinity + if sign == 0 { + f64::INFINITY + } else { + f64::NEG_INFINITY + } + } else if new_exp <= 0 { + // Underflow to zero + if sign == 0 { 0.0 } else { -0.0 } + } else { + // Normal f64 + // Mantissa: take top 52 bits of 112-bit mantissa + let new_mant = (mant >> (112 - 52)) as u64; + let bits = (sign << 63) | ((new_exp as u64) << 52) | new_mant; + f64::from_bits(bits) + } + } } #[cfg(test)] diff --git a/src/uu/od/src/multifile_reader.rs b/src/uu/od/src/multifile_reader.rs index 7d4709ce1..48e1f1225 100644 --- a/src/uu/od/src/multifile_reader.rs +++ b/src/uu/od/src/multifile_reader.rs @@ -87,7 +87,13 @@ impl MultifileReader<'_> { // print an error at the time that the file is needed, // then move to the next file. // This matches the behavior of the original `od` - show_error!("{}: {e}", fname.maybe_quote()); + // Format error without OS error code to match GNU od + let error_msg = match e.kind() { + io::ErrorKind::NotFound => "No such file or directory", + io::ErrorKind::PermissionDenied => "Permission denied", + _ => "I/O error", + }; + show_error!("{}: {}", fname.maybe_quote().external(true), error_msg); self.any_err = true; } } diff --git a/src/uu/od/src/od.rs b/src/uu/od/src/od.rs index 80e6893d1..e8f9841b1 100644 --- a/src/uu/od/src/od.rs +++ b/src/uu/od/src/od.rs @@ -80,14 +80,19 @@ struct OdOptions { } /// Helper function to parse bytes with error handling -fn parse_bytes_option(matches: &ArgMatches, option_name: &str) -> UResult> { +fn parse_bytes_option( + matches: &ArgMatches, + args: &[String], + option_name: &str, + short: Option, +) -> UResult> { match matches.get_one::(option_name) { None => Ok(None), Some(s) => match parse_number_of_bytes(s) { Ok(n) => Ok(Some(n)), Err(e) => Err(USimpleError::new( 1, - format_error_message(&e, s, option_name), + format_error_message(&e, s, &option_display_name(args, option_name, short)), )), }, } @@ -110,12 +115,12 @@ impl OdOptions { ByteOrder::Native }; - let mut skip_bytes = parse_bytes_option(matches, options::SKIP_BYTES)?.unwrap_or(0); + let mut skip_bytes = + parse_bytes_option(matches, args, options::SKIP_BYTES, Some('j'))?.unwrap_or(0); let mut label: Option = None; - let parsed_input = parse_inputs(matches) - .map_err(|e| USimpleError::new(1, translate!("od-error-invalid-inputs", "msg" => e)))?; + let parsed_input = parse_inputs(matches).map_err(|e| USimpleError::new(1, e))?; let input_strings = match parsed_input { CommandLineInputs::FileNames(v) => v, CommandLineInputs::FileAndOffset((f, s, l)) => { @@ -131,16 +136,30 @@ impl OdOptions { None => 16, Some(s) => { if matches.value_source(options::WIDTH) == Some(ValueSource::CommandLine) { - match parse_number_of_bytes(s) { - Ok(n) => usize::try_from(n) - .map_err(|_| USimpleError::new(1, format!("‘{s}‘ is too large")))?, - Err(e) => { - return Err(USimpleError::new( - 1, - format_error_message(&e, s, options::WIDTH), - )); - } + let width_display = option_display_name(args, options::WIDTH, Some('w')); + let parsed = parse_number_of_bytes(s).map_err(|e| { + USimpleError::new(1, format_error_message(&e, s, &width_display)) + })?; + if parsed == 0 { + return Err(USimpleError::new( + 1, + translate!( + "od-error-invalid-argument", + "option" => width_display.clone(), + "value" => s.quote() + ), + )); } + usize::try_from(parsed).map_err(|_| { + USimpleError::new( + 1, + translate!( + "od-error-argument-too-large", + "option" => width_display.clone(), + "value" => s.quote() + ), + ) + })? } else { 16 } @@ -160,9 +179,9 @@ impl OdOptions { let output_duplicates = matches.get_flag(options::OUTPUT_DUPLICATES); - let read_bytes = parse_bytes_option(matches, options::READ_BYTES)?; + let read_bytes = parse_bytes_option(matches, args, options::READ_BYTES, Some('N'))?; - let string_min_length = match parse_bytes_option(matches, options::STRINGS)? { + let string_min_length = match parse_bytes_option(matches, args, options::STRINGS, Some('S'))? { None => None, Some(n) => Some(usize::try_from(n).map_err(|_| { USimpleError::new( @@ -491,7 +510,9 @@ where let length = memory_decoder.length(); if length == 0 { - input_offset.print_final_offset(); + if !input_decoder.has_error() { + input_offset.print_final_offset(); + } break; } @@ -669,6 +690,10 @@ fn print_bytes(prefix: &str, input_decoder: &MemoryDecoder, output_info: &Output let p = input_decoder.read_float(b, f.formatter_item_info.byte_size); output_text.push_str(&func(p)); } + FormatWriter::LongDoubleWriter(func) => { + let p = input_decoder.read_long_double(b); + output_text.push_str(&func(p)); + } FormatWriter::BFloatWriter(func) => { let p = input_decoder.read_bfloat(b); output_text.push_str(&func(p)); @@ -745,6 +770,27 @@ impl HasError for BufReader { } } +fn option_display_name(args: &[String], option_name: &str, short: Option) -> String { + let long_form = format!("--{option_name}"); + let long_form_with_eq = format!("{long_form}="); + if let Some(short_char) = short { + let short_form = format!("-{short_char}"); + for arg in args.iter().skip(1) { + if !arg.starts_with("--") && arg.starts_with(&short_form) { + return short_form; + } + } + for arg in args.iter().skip(1) { + if arg == &long_form || arg.starts_with(&long_form_with_eq) { + return long_form; + } + } + short_form + } else { + long_form + } +} + fn format_error_message(error: &ParseSizeError, s: &str, option: &str) -> String { // NOTE: // GNU's od echos affected flag, -N or --read-bytes (-j or --skip-bytes, etc.), depending user's selection diff --git a/src/uu/od/src/output_info.rs b/src/uu/od/src/output_info.rs index 38218cde8..ef63c1602 100644 --- a/src/uu/od/src/output_info.rs +++ b/src/uu/od/src/output_info.rs @@ -11,7 +11,7 @@ use crate::formatter_item_info::FormatterItemInfo; use crate::parse_formats::ParsedFormatterItemInfo; /// Size in bytes of the max datatype. ie set to 16 for 128-bit numbers. -const MAX_BYTES_PER_UNIT: usize = 8; +const MAX_BYTES_PER_UNIT: usize = 16; /// Contains information to output single output line in human readable form pub struct SpacedFormatterItemInfo { @@ -204,6 +204,36 @@ impl TypeSizeInfo for TypeInfo { } } +#[cfg(test)] +fn assert_alignment( + expected: &[usize], + type_info: TypeInfo, + byte_size_block: usize, + print_width_block: usize, +) { + assert_eq!( + expected.len(), + byte_size_block, + "expected spacing must describe every byte in the block" + ); + + let spacing = OutputInfo::calculate_alignment(&type_info, byte_size_block, print_width_block); + + assert_eq!( + expected, + &spacing[..byte_size_block], + "unexpected spacing for byte_size={} print_width={} block_width={}", + type_info.byte_size, + type_info.print_width, + print_width_block + ); + assert!( + spacing[byte_size_block..].iter().all(|&s| s == 0), + "spacing beyond the active block should remain zero: {:?}", + &spacing[byte_size_block..] + ); +} + #[test] #[allow(clippy::cognitive_complexity)] fn test_calculate_alignment() { @@ -213,40 +243,34 @@ fn test_calculate_alignment() { // ffff ffff ffff ffff ffff ffff ffff ffff // the first line has no additional spacing: - assert_eq!( - [0, 0, 0, 0, 0, 0, 0, 0], - OutputInfo::calculate_alignment( - &TypeInfo { - byte_size: 8, - print_width: 23, - }, - 8, - 23 - ) + assert_alignment( + &[0, 0, 0, 0, 0, 0, 0, 0], + TypeInfo { + byte_size: 8, + print_width: 23, + }, + 8, + 23, ); // the second line a single space at the start of the block: - assert_eq!( - [1, 0, 0, 0, 0, 0, 0, 0], - OutputInfo::calculate_alignment( - &TypeInfo { - byte_size: 4, - print_width: 11, - }, - 8, - 23 - ) + assert_alignment( + &[1, 0, 0, 0, 0, 0, 0, 0], + TypeInfo { + byte_size: 4, + print_width: 11, + }, + 8, + 23, ); // the third line two spaces at pos 0, and 1 space at pos 4: - assert_eq!( - [2, 0, 0, 0, 1, 0, 0, 0], - OutputInfo::calculate_alignment( - &TypeInfo { - byte_size: 2, - print_width: 5, - }, - 8, - 23 - ) + assert_alignment( + &[2, 0, 0, 0, 1, 0, 0, 0], + TypeInfo { + byte_size: 2, + print_width: 5, + }, + 8, + 23, ); // For this example `byte_size_block` is 8 and 'print_width_block' is 28: @@ -255,195 +279,161 @@ fn test_calculate_alignment() { // 177777 177777 177777 177777 177777 177777 177777 177777 // ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff - assert_eq!( - [7, 0, 0, 0, 0, 0, 0, 0], - OutputInfo::calculate_alignment( - &TypeInfo { - byte_size: 8, - print_width: 21, - }, - 8, - 28 - ) + assert_alignment( + &[7, 0, 0, 0, 0, 0, 0, 0], + TypeInfo { + byte_size: 8, + print_width: 21, + }, + 8, + 28, ); - assert_eq!( - [5, 0, 0, 0, 5, 0, 0, 0], - OutputInfo::calculate_alignment( - &TypeInfo { - byte_size: 4, - print_width: 9, - }, - 8, - 28 - ) + assert_alignment( + &[5, 0, 0, 0, 5, 0, 0, 0], + TypeInfo { + byte_size: 4, + print_width: 9, + }, + 8, + 28, ); - assert_eq!( - [0, 0, 0, 0, 0, 0, 0, 0], - OutputInfo::calculate_alignment( - &TypeInfo { - byte_size: 2, - print_width: 7, - }, - 8, - 28 - ) + assert_alignment( + &[0, 0, 0, 0, 0, 0, 0, 0], + TypeInfo { + byte_size: 2, + print_width: 7, + }, + 8, + 28, ); - assert_eq!( - [1, 0, 1, 0, 1, 0, 1, 0], - OutputInfo::calculate_alignment( - &TypeInfo { - byte_size: 1, - print_width: 3, - }, - 8, - 28 - ) + assert_alignment( + &[1, 0, 1, 0, 1, 0, 1, 0], + TypeInfo { + byte_size: 1, + print_width: 3, + }, + 8, + 28, ); // 9 tests where 8 .. 16 spaces are spread across 8 positions - assert_eq!( - [1, 1, 1, 1, 1, 1, 1, 1], - OutputInfo::calculate_alignment( - &TypeInfo { - byte_size: 1, - print_width: 2, - }, - 8, - 16 + 8 - ) + assert_alignment( + &[1, 1, 1, 1, 1, 1, 1, 1], + TypeInfo { + byte_size: 1, + print_width: 2, + }, + 8, + 16 + 8, ); - assert_eq!( - [2, 1, 1, 1, 1, 1, 1, 1], - OutputInfo::calculate_alignment( - &TypeInfo { - byte_size: 1, - print_width: 2, - }, - 8, - 16 + 9 - ) + assert_alignment( + &[2, 1, 1, 1, 1, 1, 1, 1], + TypeInfo { + byte_size: 1, + print_width: 2, + }, + 8, + 16 + 9, ); - assert_eq!( - [2, 1, 1, 1, 2, 1, 1, 1], - OutputInfo::calculate_alignment( - &TypeInfo { - byte_size: 1, - print_width: 2, - }, - 8, - 16 + 10 - ) + assert_alignment( + &[2, 1, 1, 1, 2, 1, 1, 1], + TypeInfo { + byte_size: 1, + print_width: 2, + }, + 8, + 16 + 10, ); - assert_eq!( - [3, 1, 1, 1, 2, 1, 1, 1], - OutputInfo::calculate_alignment( - &TypeInfo { - byte_size: 1, - print_width: 2, - }, - 8, - 16 + 11 - ) + assert_alignment( + &[3, 1, 1, 1, 2, 1, 1, 1], + TypeInfo { + byte_size: 1, + print_width: 2, + }, + 8, + 16 + 11, ); - assert_eq!( - [2, 1, 2, 1, 2, 1, 2, 1], - OutputInfo::calculate_alignment( - &TypeInfo { - byte_size: 1, - print_width: 2, - }, - 8, - 16 + 12 - ) + assert_alignment( + &[2, 1, 2, 1, 2, 1, 2, 1], + TypeInfo { + byte_size: 1, + print_width: 2, + }, + 8, + 16 + 12, ); - assert_eq!( - [3, 1, 2, 1, 2, 1, 2, 1], - OutputInfo::calculate_alignment( - &TypeInfo { - byte_size: 1, - print_width: 2, - }, - 8, - 16 + 13 - ) + assert_alignment( + &[3, 1, 2, 1, 2, 1, 2, 1], + TypeInfo { + byte_size: 1, + print_width: 2, + }, + 8, + 16 + 13, ); - assert_eq!( - [3, 1, 2, 1, 3, 1, 2, 1], - OutputInfo::calculate_alignment( - &TypeInfo { - byte_size: 1, - print_width: 2, - }, - 8, - 16 + 14 - ) + assert_alignment( + &[3, 1, 2, 1, 3, 1, 2, 1], + TypeInfo { + byte_size: 1, + print_width: 2, + }, + 8, + 16 + 14, ); - assert_eq!( - [4, 1, 2, 1, 3, 1, 2, 1], - OutputInfo::calculate_alignment( - &TypeInfo { - byte_size: 1, - print_width: 2, - }, - 8, - 16 + 15 - ) + assert_alignment( + &[4, 1, 2, 1, 3, 1, 2, 1], + TypeInfo { + byte_size: 1, + print_width: 2, + }, + 8, + 16 + 15, ); - assert_eq!( - [2, 2, 2, 2, 2, 2, 2, 2], - OutputInfo::calculate_alignment( - &TypeInfo { - byte_size: 1, - print_width: 2, - }, - 8, - 16 + 16 - ) + assert_alignment( + &[2, 2, 2, 2, 2, 2, 2, 2], + TypeInfo { + byte_size: 1, + print_width: 2, + }, + 8, + 16 + 16, ); // 4 tests where 15 spaces are spread across 8, 4, 2 or 1 position(s) - assert_eq!( - [4, 1, 2, 1, 3, 1, 2, 1], - OutputInfo::calculate_alignment( - &TypeInfo { - byte_size: 1, - print_width: 2, - }, - 8, - 16 + 15 - ) + assert_alignment( + &[4, 1, 2, 1, 3, 1, 2, 1], + TypeInfo { + byte_size: 1, + print_width: 2, + }, + 8, + 16 + 15, ); - assert_eq!( - [5, 0, 3, 0, 4, 0, 3, 0], - OutputInfo::calculate_alignment( - &TypeInfo { - byte_size: 2, - print_width: 4, - }, - 8, - 16 + 15 - ) + assert_alignment( + &[5, 0, 3, 0, 4, 0, 3, 0], + TypeInfo { + byte_size: 2, + print_width: 4, + }, + 8, + 16 + 15, ); - assert_eq!( - [8, 0, 0, 0, 7, 0, 0, 0], - OutputInfo::calculate_alignment( - &TypeInfo { - byte_size: 4, - print_width: 8, - }, - 8, - 16 + 15 - ) + assert_alignment( + &[8, 0, 0, 0, 7, 0, 0, 0], + TypeInfo { + byte_size: 4, + print_width: 8, + }, + 8, + 16 + 15, ); - assert_eq!( - [15, 0, 0, 0, 0, 0, 0, 0], - OutputInfo::calculate_alignment( - &TypeInfo { - byte_size: 8, - print_width: 16, - }, - 8, - 16 + 15 - ) + assert_alignment( + &[15, 0, 0, 0, 0, 0, 0, 0], + TypeInfo { + byte_size: 8, + print_width: 16, + }, + 8, + 16 + 15, ); } diff --git a/src/uu/od/src/parse_formats.rs b/src/uu/od/src/parse_formats.rs index a62adc1ad..0edb48474 100644 --- a/src/uu/od/src/parse_formats.rs +++ b/src/uu/od/src/parse_formats.rs @@ -81,6 +81,7 @@ fn od_format_type(type_char: FormatType, byte_size: u8) -> Option Some(FORMAT_ITEM_F16), (FormatType::Float, 0 | 4) => Some(FORMAT_ITEM_F32), (FormatType::Float, 8) => Some(FORMAT_ITEM_F64), + (FormatType::Float, 16) => Some(FORMAT_ITEM_LONG_DOUBLE), _ => None, } @@ -238,7 +239,10 @@ fn is_format_size_char( *byte_size = 2; true } - // FormatTypeCategory::Float, 'L' => *byte_size = 16, // TODO support f128 + (FormatTypeCategory::Float, Some('L')) => { + *byte_size = 16; + true + } _ => false, } } diff --git a/src/uu/od/src/parse_inputs.rs b/src/uu/od/src/parse_inputs.rs index b185e5427..8f5e6434b 100644 --- a/src/uu/od/src/parse_inputs.rs +++ b/src/uu/od/src/parse_inputs.rs @@ -69,17 +69,30 @@ pub fn parse_inputs(matches: &dyn CommandLineOpts) -> Result { + // if there is just 1 input (stdin), an offset must start with '+' + if input_strings.len() == 1 && input_strings[0].starts_with('+') { + return Ok(CommandLineInputs::FileAndOffset(("-".to_string(), n, None))); + } + if input_strings.len() == 2 { + return Ok(CommandLineInputs::FileAndOffset(( + input_strings[0].to_string(), + n, + None, + ))); + } } - if input_strings.len() == 2 { - return Ok(CommandLineInputs::FileAndOffset(( - input_strings[0].to_string(), - n, - None, - ))); + Err(e) => { + // If it's an overflow error, propagate it + // Otherwise, treat it as a filename + let err = std::io::Error::from_raw_os_error(libc::ERANGE); + let msg = err.to_string(); + let expected_msg = msg.split(" (os error").next().unwrap_or(&msg).to_string(); + + if e == expected_msg { + return Err(format!("{}: {}", input_strings[input_strings.len() - 1], e)); + } } } } @@ -123,7 +136,7 @@ pub fn parse_inputs_traditional(input_strings: &[&str]) -> Result Err(translate!("od-error-invalid-offset", "offset" => input_strings[1])), + (_, Err(e)) => Err(format!("{}: {}", input_strings[1], e)), } } 3 => { @@ -135,12 +148,8 @@ pub fn parse_inputs_traditional(input_strings: &[&str]) -> Result { - Err(translate!("od-error-invalid-offset", "offset" => input_strings[1])) - } - (_, Err(_)) => { - Err(translate!("od-error-invalid-label", "label" => input_strings[2])) - } + (Err(e), _) => Err(format!("{}: {}", input_strings[1], e)), + (_, Err(e)) => Err(format!("{}: {}", input_strings[2], e)), } } _ => Err(translate!("od-error-too-many-inputs", "input" => input_strings[3])), @@ -148,7 +157,24 @@ pub fn parse_inputs_traditional(input_strings: &[&str]) -> Result Result { +pub fn parse_offset_operand(s: &str) -> Result { + if s.is_empty() { + return Err(translate!("od-error-parse-failed")); + } + + if s.contains(' ') { + return Err(translate!("od-error-parse-failed")); + } + + if s.starts_with("++") || s.starts_with("+-") { + return Err(translate!("od-error-parse-failed")); + } + + // Reject strings starting with "-" (negative numbers not allowed) + if s.starts_with('-') { + return Err(translate!("od-error-parse-failed")); + } + let mut start = 0; let mut len = s.len(); let mut radix = 8; @@ -171,9 +197,40 @@ pub fn parse_offset_operand(s: &str) -> Result { radix = 10; } } + + // Check if the substring is empty after processing prefixes/suffixes + if start >= len { + return Err(translate!("od-error-parse-failed")); + } + match u64::from_str_radix(&s[start..len], radix) { - Ok(i) => Ok(i * multiply), - Err(_) => Err(translate!("od-error-parse-failed").leak()), + Ok(i) => { + // Check for overflow during multiplication + match i.checked_mul(multiply) { + Some(result) => Ok(result), + None => { + let err = std::io::Error::from_raw_os_error(libc::ERANGE); + let msg = err.to_string(); + // Strip "(os error N)" if present to match Perl's $! + let msg = msg.split(" (os error").next().unwrap_or(&msg).to_string(); + Err(msg) + } + } + } + Err(e) => { + // Distinguish between overflow and parse failure + // from_str_radix returns IntErrorKind::PosOverflow for overflow + use std::num::IntErrorKind; + match e.kind() { + IntErrorKind::PosOverflow => { + let err = std::io::Error::from_raw_os_error(libc::ERANGE); + let msg = err.to_string(); + let msg = msg.split(" (os error").next().unwrap_or(&msg).to_string(); + Err(msg) + } + _ => Err(translate!("od-error-parse-failed")), + } + } } } @@ -340,7 +397,7 @@ mod tests { .unwrap_err(); } - fn parse_offset_operand_str(s: &str) -> Result { + fn parse_offset_operand_str(s: &str) -> Result { parse_offset_operand(&String::from(s)) } diff --git a/src/uu/od/src/prn_float.rs b/src/uu/od/src/prn_float.rs index 2e1ff6988..155ce7d07 100644 --- a/src/uu/od/src/prn_float.rs +++ b/src/uu/od/src/prn_float.rs @@ -2,7 +2,7 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use half::f16; +use half::{bf16, f16}; use std::num::FpCategory; use crate::formatter_item_info::{FormatWriter, FormatterItemInfo}; @@ -25,6 +25,12 @@ pub static FORMAT_ITEM_F64: FormatterItemInfo = FormatterItemInfo { formatter: FormatWriter::FloatWriter(format_item_f64), }; +pub static FORMAT_ITEM_LONG_DOUBLE: FormatterItemInfo = FormatterItemInfo { + byte_size: 16, + print_width: 40, + formatter: FormatWriter::LongDoubleWriter(format_item_long_double), +}; + pub static FORMAT_ITEM_BF16: FormatterItemInfo = FormatterItemInfo { byte_size: 2, print_width: 16, @@ -43,6 +49,10 @@ pub fn format_item_f64(f: f64) -> String { format!(" {}", format_f64(f)) } +pub fn format_item_long_double(f: f64) -> String { + format!(" {}", format_long_double(f)) +} + fn format_f32_exp(f: f32, width: usize) -> String { if f.abs().log10() < 0.0 { return format!("{f:width$e}"); @@ -71,11 +81,30 @@ fn format_f64_exp_precision(f: f64, width: usize, precision: usize) -> String { } pub fn format_item_bf16(f: f64) -> String { - format!(" {}", format_f32(f as f32)) + let bf = bf16::from_f32(f as f32); + format!(" {}", format_binary16_like(f, 15, 8, is_subnormal_bf16(bf))) } fn format_f16(f: f16) -> String { - format_float(f64::from(f), 15, 8) + let value = f64::from(f); + format_binary16_like(value, 15, 8, is_subnormal_f16(f)) +} + +fn format_binary16_like(value: f64, width: usize, precision: usize, force_exp: bool) -> String { + if force_exp { + return format_f64_exp_precision(value, width, precision - 1); + } + format_float(value, width, precision) +} + +fn is_subnormal_f16(value: f16) -> bool { + let bits = value.to_bits(); + (bits & 0x7C00) == 0 && (bits & 0x03FF) != 0 +} + +fn is_subnormal_bf16(value: bf16) -> bool { + let bits = value.to_bits(); + (bits & 0x7F80) == 0 && (bits & 0x007F) != 0 } /// formats float with 8 significant digits, eg 12345678 or -1.2345678e+12 @@ -124,6 +153,34 @@ fn format_float(f: f64, width: usize, precision: usize) -> String { } } +fn format_long_double(f: f64) -> String { + // On most platforms, long double is either 64-bit (same as f64) or 80-bit/128-bit + // Since we're reading it as f64, we format it with extended precision + // Width is 39 (40 - 1 for leading space), precision is 21 significant digits + let width: usize = 39; + let precision: usize = 21; + + // Handle special cases + if f.is_nan() { + return format!("{:>width$}", "NaN"); + } + if f.is_infinite() { + if f.is_sign_negative() { + return format!("{:>width$}", "-inf"); + } + return format!("{:>width$}", "inf"); + } + if f == 0.0 { + if f.is_sign_negative() { + return format!("{:>width$}", "-0"); + } + return format!("{:>width$}", "0"); + } + + // For normal numbers, format with appropriate precision using exponential notation + format!("{f:>width$.precision$e}") +} + #[test] #[allow(clippy::excessive_precision)] #[allow(clippy::cognitive_complexity)] diff --git a/tests/by-util/test_od.rs b/tests/by-util/test_od.rs index d5b747948..54be34551 100644 --- a/tests/by-util/test_od.rs +++ b/tests/by-util/test_od.rs @@ -7,6 +7,8 @@ #[cfg(unix)] use std::io::Read; +#[cfg(target_os = "linux")] +use std::path::Path; use unindent::unindent; use uutests::util::TestScenario; @@ -19,6 +21,27 @@ static ALPHA_OUT: &str = " 0000033 "; +fn erange_message() -> String { + let err = std::io::Error::from_raw_os_error(libc::ERANGE); + let msg = err.to_string(); + msg.split(" (os error").next().unwrap_or(&msg).to_string() +} + +fn run_skip_across_inputs(files: &[(&str, &str)], skip: u64, expected: &str) { + let (at, mut ucmd) = at_and_ucmd!(); + for (name, contents) in files { + at.write(name, contents); + } + + ucmd.arg("-c").arg("-j").arg(skip.to_string()).arg("-An"); + + for (name, _) in files { + ucmd.arg(name); + } + + ucmd.succeeds().stdout_only(expected); +} + #[test] fn test_invalid_arg() { new_ucmd!().arg("--definitely-invalid").fails_with_code(1); @@ -368,22 +391,29 @@ fn test_invalid_width() { #[test] fn test_zero_width() { - let input: [u8; 4] = [0x00, 0x00, 0x00, 0x00]; - let expected_output = unindent( - " - 0000000 000000 - 0000002 000000 - 0000004 - ", - ); - new_ucmd!() .arg("-w0") - .arg("-v") - .run_piped_stdin(&input[..]) - .success() - .stderr_is_bytes("od: warning: invalid width 0; using 2 instead\n".as_bytes()) - .stdout_is(expected_output); + .arg("-An") + .fails_with_code(1) + .stderr_only("od: invalid -w argument '0'\n"); +} + +#[test] +fn test_negative_width_argument() { + new_ucmd!() + .arg("-w-1") + .arg("-An") + .fails_with_code(1) + .stderr_only("od: invalid -w argument '-1'\n"); +} + +#[test] +fn test_non_numeric_width_argument() { + new_ucmd!() + .arg("-ww") + .arg("-An") + .fails_with_code(1) + .stderr_only("od: invalid -w argument 'w'\n"); } #[test] @@ -402,6 +432,42 @@ fn test_width_without_value() { .stdout_only(expected_output); } +#[test] +fn test_very_wide_ascii_output() { + let (at, mut ucmd) = at_and_ucmd!(); + at.write("data-a", "x"); + ucmd.arg("-a") + .arg("-w65537") + .arg("-An") + .arg("data-a") + .succeeds() + .stdout_only(" x\n"); +} + +#[test] +fn test_very_wide_char_output() { + let (at, mut ucmd) = at_and_ucmd!(); + at.write("data-c", "x"); + ucmd.arg("-c") + .arg("-w65537") + .arg("-An") + .arg("data-c") + .succeeds() + .stdout_only(" x\n"); +} + +#[test] +fn test_very_wide_hex_byte_output() { + let (at, mut ucmd) = at_and_ucmd!(); + at.write_bytes("data-x", &[0x42]); + ucmd.arg("-tx1") + .arg("-w65537") + .arg("-An") + .arg("data-x") + .succeeds() + .stdout_only(" 42\n"); +} + #[test] fn test_suppress_duplicates() { let input: [u8; 41] = [ @@ -606,6 +672,53 @@ fn test_invalid_offset() { new_ucmd!().arg("-Ab").fails(); } +#[test] +fn test_invalid_traditional_offsets_are_filenames() { + let cases = [("++0", "++0"), ("+-0", "+-0"), ("+ 0", "'+ 0'")]; + + for (input, display) in cases { + new_ucmd!() + .arg(input) + .fails_with_code(1) + .stderr_only(format!("od: {display}: No such file or directory\n")); + } + + new_ucmd!() + .arg("--") + .arg("-0") + .fails_with_code(1) + .stderr_only("od: -0: No such file or directory\n"); +} + +#[test] +fn test_traditional_offset_overflow_diagnosed() { + let erange = erange_message(); + let long_octal = "7".repeat(255); + let long_decimal = format!("{}.", "9".repeat(254)); + let long_hex = format!("0x{}", "f".repeat(253)); + + new_ucmd!() + .arg("-") + .arg(&long_octal) + .pipe_in(Vec::::new()) + .fails_with_code(1) + .stderr_only(format!("od: {long_octal}: {erange}\n")); + + new_ucmd!() + .arg("-") + .arg(&long_decimal) + .pipe_in(Vec::::new()) + .fails_with_code(1) + .stderr_only(format!("od: {long_decimal}: {erange}\n")); + + new_ucmd!() + .arg("-") + .arg(&long_hex) + .pipe_in(Vec::::new()) + .fails_with_code(1) + .stderr_only(format!("od: {long_hex}: {erange}\n")); +} + #[test] fn test_empty_offset() { new_ucmd!() @@ -670,6 +783,59 @@ fn test_skip_bytes_hex() { )); } +#[test] +fn test_skip_bytes_consumes_single_input() { + run_skip_across_inputs(&[("g", "a")], 1, ""); +} + +#[test] +fn test_skip_bytes_consumes_two_inputs() { + run_skip_across_inputs(&[("g", "a"), ("h", "b")], 2, ""); +} + +#[test] +fn test_skip_bytes_consumes_three_inputs() { + run_skip_across_inputs(&[("g", "a"), ("h", "b"), ("i", "c")], 3, ""); +} + +#[test] +fn test_skip_bytes_prints_after_consuming_multiple_inputs() { + run_skip_across_inputs( + &[("g", "a"), ("h", "b"), ("i", "c"), ("j", "d")], + 3, + " d\n", + ); +} + +#[cfg(target_os = "linux")] +#[test] +fn test_skip_bytes_proc_file_without_seeking() { + let proc_path = Path::new("/proc/version"); + if !proc_path.exists() { + return; + } + + let Ok(contents) = std::fs::read(proc_path) else { + return; + }; + + if contents.is_empty() { + return; + } + + let (at, mut ucmd) = at_and_ucmd!(); + at.write("after", "e"); + + ucmd.arg("-An") + .arg("-c") + .arg("-j") + .arg(contents.len().to_string()) + .arg(proc_path) + .arg("after") + .succeeds() + .stdout_only(" e\n"); +} + #[test] fn test_skip_bytes_error() { let input = "12345"; @@ -778,6 +944,24 @@ fn test_stdin_offset() { )); } +#[test] +fn test_traditional_decimal_dot_offset() { + new_ucmd!() + .arg("+1.") + .pipe_in("a") + .succeeds() + .stdout_only("0000001\n"); +} + +#[test] +fn test_traditional_dot_block_offset() { + new_ucmd!() + .arg("+1.b") + .pipe_in(vec![b'a'; 512]) + .succeeds() + .stdout_only("0001000\n"); +} + #[test] fn test_file_offset() { new_ucmd!() From 4429d44ca150cbe68d756eb210cbdaf960bf801e Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Fri, 28 Nov 2025 16:54:04 +0900 Subject: [PATCH 126/182] CICD.yml: Dedup a mkdir --- .github/workflows/CICD.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index 04917d9ef..49ffe45c8 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -712,7 +712,6 @@ jobs: shell: bash run: | ## Create build/work space - mkdir -p '${{ steps.vars.outputs.STAGING }}' mkdir -p '${{ steps.vars.outputs.STAGING }}/${{ steps.vars.outputs.PKG_BASENAME }}' - name: Install/setup prerequisites shell: bash From 9b8a0c1678ec6634b586a7504c3f70a25f16e928 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Fri, 28 Nov 2025 17:12:29 +0900 Subject: [PATCH 127/182] CICD.yml: Drop a workaround for old package --- .github/workflows/CICD.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index 04917d9ef..177f22be3 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -1120,11 +1120,6 @@ jobs: ;; esac - case '${{ matrix.job.os }}' in - # Update binutils if MinGW due to https://github.com/rust-lang/rust/issues/112368 - windows-latest) C:/msys64/usr/bin/pacman.exe -Sy --needed mingw-w64-x86_64-gcc --noconfirm ; echo "C:\msys64\mingw64\bin" >> $GITHUB_PATH ;; - esac - ## Install the llvm-tools component to get access to `llvm-profdata` rustup component add llvm-tools From c4b9fa6f0848a2240830f4c825573a38cec42107 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Fri, 28 Nov 2025 18:41:09 +0900 Subject: [PATCH 128/182] why-skip.md: Remove an OOD doc --- util/why-skip.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/util/why-skip.md b/util/why-skip.md index 19310a71e..23a526029 100644 --- a/util/why-skip.md +++ b/util/why-skip.md @@ -70,9 +70,6 @@ = 512 byte aligned O_DIRECT is not supported on this (file) system = * tests/dd/direct.sh -= skipped test: /usr/bin/touch -m -d '1998-01-15 23:00' didn't work = -* tests/misc/ls-time.sh - = requires controlling input terminal = * tests/misc/stty-pairs.sh * tests/misc/stty.sh From 7618eb0e90a77269847879b441442b90f4d1da4d Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Fri, 28 Nov 2025 18:47:21 +0900 Subject: [PATCH 129/182] why-skip.md: Remove a passing test --- util/why-skip.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/util/why-skip.md b/util/why-skip.md index 19310a71e..95e35bbce 100644 --- a/util/why-skip.md +++ b/util/why-skip.md @@ -48,9 +48,6 @@ = The Swedish locale with blank thousands separator is unavailable. = * tests/misc/sort-h-thousands-sep.sh -= this shell lacks ulimit support = -* tests/misc/csplit-heap.sh - = multicall binary is disabled = * tests/misc/coreutils.sh From 8ffe61b08568e43491a5fe46b33364fc9ce85039 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Fri, 28 Nov 2025 18:58:16 +0900 Subject: [PATCH 130/182] why-skip.md: Remove 4 sparse-* --- util/why-skip.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/util/why-skip.md b/util/why-skip.md index 54633c026..7f2693fe0 100644 --- a/util/why-skip.md +++ b/util/why-skip.md @@ -71,9 +71,3 @@ * tests/misc/stty-pairs.sh * tests/misc/stty.sh * tests/misc/stty-invalid.sh - -= insufficient SEEK_DATA support = -* tests/cp/sparse-perf.sh -* tests/cp/sparse-extents.sh -* tests/cp/sparse-extents-2.sh -* tests/cp/sparse-2.sh From bc5cfeac5e40d097cf12101a07bd5aede269f9a6 Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Fri, 28 Nov 2025 11:10:55 +0100 Subject: [PATCH 131/182] Bump icu crates from 2.0.0 to 2.1.1 --- Cargo.lock | 89 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 47 insertions(+), 42 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 72b1ca0d3..a832a8bb3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1319,11 +1319,10 @@ dependencies = [ [[package]] name = "icu_collator" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ad4c6a556938dfd31f75a8c54141079e8821dc697ffb799cfe0f0fa11f2edc" +checksum = "32eed11a5572f1088b63fa21dc2e70d4a865e5739fc2d10abc05be93bae97019" dependencies = [ - "displaydoc", "icu_collator_data", "icu_collections", "icu_locale", @@ -1339,15 +1338,15 @@ dependencies = [ [[package]] name = "icu_collator_data" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d880b8e680799eabd90c054e1b95526cd48db16c95269f3c89fb3117e1ac92c5" +checksum = "5ab06f0e83a613efddba3e4913e00e43ed4001fae651cb7d40fc7e66b83b6fb9" [[package]] name = "icu_collections" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", "potential_utf", @@ -1358,34 +1357,31 @@ dependencies = [ [[package]] name = "icu_decimal" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fec61c43fdc4e368a9f450272833123a8ef0d7083a44597660ce94d791b8a2e2" +checksum = "a38c52231bc348f9b982c1868a2af3195199623007ba2c7650f432038f5b3e8e" dependencies = [ - "displaydoc", "fixed_decimal", "icu_decimal_data", "icu_locale", "icu_locale_core", "icu_provider", - "tinystr", "writeable", "zerovec", ] [[package]] name = "icu_decimal_data" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b70963bc35f9bdf1bc66a5c1f458f4991c1dc71760e00fa06016b2c76b2738d5" +checksum = "2905b4044eab2dd848fe84199f9195567b63ab3a93094711501363f63546fef7" [[package]] name = "icu_locale" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ae5921528335e91da1b6c695dbf1ec37df5ac13faa3f91e5640be93aa2fbefd" +checksum = "532b11722e350ab6bf916ba6eb0efe3ee54b932666afec989465f9243fe6dd60" dependencies = [ - "displaydoc", "icu_collections", "icu_locale_core", "icu_locale_data", @@ -1397,12 +1393,13 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", + "serde", "tinystr", "writeable", "zerovec", @@ -1410,63 +1407,63 @@ dependencies = [ [[package]] name = "icu_locale_data" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fdef0c124749d06a743c69e938350816554eb63ac979166590e2b4ee4252765" +checksum = "f03e2fcaefecdf05619f3d6f91740e79ab969b4dd54f77cbf546b1d0d28e3147" [[package]] name = "icu_normalizer" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", "icu_provider", "smallvec", + "utf16_iter", + "utf8_iter", + "write16", "zerovec", ] [[package]] name = "icu_normalizer_data" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" dependencies = [ - "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", - "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" [[package]] name = "icu_provider" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", "icu_locale_core", + "serde", "stable_deref_trait", - "tinystr", "writeable", "yoke", "zerofrom", @@ -2127,11 +2124,12 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.2" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" dependencies = [ - "serde", + "serde_core", + "writeable", "zerovec", ] @@ -4659,10 +4657,16 @@ dependencies = [ ] [[package]] -name = "writeable" -version = "0.6.1" +name = "write16" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "wyz" @@ -4794,10 +4798,11 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.2" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ + "serde", "yoke", "zerofrom", "zerovec-derive", From e5ec330859bb758cd5c190d02a9d953e02cdc6ef Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Fri, 28 Nov 2025 21:55:26 +0900 Subject: [PATCH 132/182] Merge pull request #9509 from oech3/patch-2 why-skip.md: Remove 3 tests --- util/why-skip.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/util/why-skip.md b/util/why-skip.md index 7f2693fe0..097fe10b6 100644 --- a/util/why-skip.md +++ b/util/why-skip.md @@ -33,9 +33,6 @@ * tests/cp/no-ctx.sh * tests/cp/cp-a-selinux.sh -= failed to set xattr of file = -* tests/misc/xattr.sh - = timeout returned 142. SIGALRM not handled? = * tests/misc/timeout-group.sh @@ -54,9 +51,6 @@ = not running on GNU/Hurd = * tests/id/gnu-zero-uids.sh -= file system cannot represent big timestamps = -* tests/du/bigtime.sh - = no rootfs in mtab = * tests/df/skip-rootfs.sh @@ -64,9 +58,6 @@ * tests/df/problematic-chars.sh * tests/cp/cp-mv-enotsup-xattr.sh -= 512 byte aligned O_DIRECT is not supported on this (file) system = -* tests/dd/direct.sh - = requires controlling input terminal = * tests/misc/stty-pairs.sh * tests/misc/stty.sh From ade2dc53e443218a0d90524edec1d3608a630b66 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Fri, 28 Nov 2025 21:55:53 +0900 Subject: [PATCH 133/182] why-error.md: Cleanup (#9510) --- util/why-error.md | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/util/why-error.md b/util/why-error.md index ac1e10ce6..73a490090 100644 --- a/util/why-error.md +++ b/util/why-error.md @@ -1,48 +1,26 @@ This file documents why some tests are failing: * gnu/tests/cp/preserve-gid.sh -* gnu/tests/csplit/csplit-suppress-matched.pl * gnu/tests/date/date-debug.sh -* gnu/tests/date/date-next-dow.pl -* gnu/tests/date/date-tz.sh * gnu/tests/date/date.pl -* gnu/tests/dd/direct.sh * gnu/tests/dd/no-allocate.sh * gnu/tests/dd/nocache_eof.sh * gnu/tests/dd/skip-seek-past-file.sh - https://github.com/uutils/coreutils/issues/7216 * gnu/tests/dd/stderr.sh -* gnu/tests/du/long-from-unreadable.sh - https://github.com/uutils/coreutils/issues/7217 -* gnu/tests/du/move-dir-while-traversing.sh -* gnu/tests/expr/expr-multibyte.pl -* gnu/tests/fmt/goal-option.sh * gnu/tests/fmt/non-space.sh -* gnu/tests/head/head-elide-tail.pl -* gnu/tests/head/head-pos.sh * gnu/tests/help/help-version-getopt.sh * gnu/tests/help/help-version.sh -* gnu/tests/install/install-C.sh - https://github.com/uutils/coreutils/pull/7215 * gnu/tests/ls/ls-misc.pl * gnu/tests/ls/stat-free-symlinks.sh * gnu/tests/misc/close-stdout.sh -* gnu/tests/misc/comm.pl * gnu/tests/misc/nohup.sh * gnu/tests/numfmt/numfmt.pl - https://github.com/uutils/coreutils/issues/7219 / https://github.com/uutils/coreutils/issues/7221 * gnu/tests/misc/stdbuf.sh - https://github.com/uutils/coreutils/issues/7072 -* gnu/tests/misc/tee.sh - https://github.com/uutils/coreutils/issues/7073 -* gnu/tests/misc/time-style.sh * gnu/tests/misc/tsort.pl - https://github.com/uutils/coreutils/issues/7074 * gnu/tests/misc/write-errors.sh -* gnu/tests/mv/hard-link-1.sh -* gnu/tests/mv/mv-special-1.sh - https://github.com/uutils/coreutils/issues/7076 -* gnu/tests/mv/part-fail.sh -* gnu/tests/mv/part-hardlink.sh -* gnu/tests/od/od-N.sh * gnu/tests/od/od-float.sh -* gnu/tests/printf/printf-quote.sh * gnu/tests/ptx/ptx-overrun.sh * gnu/tests/ptx/ptx.pl -* gnu/tests/rm/empty-inacc.sh - https://github.com/uutils/coreutils/issues/7033 -* gnu/tests/rm/ir-1.sh * gnu/tests/rm/one-file-system.sh - https://github.com/uutils/coreutils/issues/7011 * gnu/tests/rm/rm1.sh * gnu/tests/rm/rm2.sh From 045cc10a642eb655945736105d153a387c96f0ca Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Fri, 28 Nov 2025 22:16:27 +0900 Subject: [PATCH 134/182] why-error.md: Cleanup and documenting (#9512) --- util/why-error.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/util/why-error.md b/util/why-error.md index 73a490090..73073c5e4 100644 --- a/util/why-error.md +++ b/util/why-error.md @@ -22,23 +22,19 @@ This file documents why some tests are failing: * gnu/tests/ptx/ptx-overrun.sh * gnu/tests/ptx/ptx.pl * gnu/tests/rm/one-file-system.sh - https://github.com/uutils/coreutils/issues/7011 -* gnu/tests/rm/rm1.sh -* gnu/tests/rm/rm2.sh +* gnu/tests/rm/rm1.sh - https://github.com/uutils/coreutils/issues/9479 * gnu/tests/shred/shred-passes.sh * gnu/tests/sort/sort-continue.sh * gnu/tests/sort/sort-debug-keys.sh * gnu/tests/sort/sort-debug-warn.sh -* gnu/tests/sort/sort-files0-from.pl * gnu/tests/sort/sort-float.sh * gnu/tests/sort/sort-h-thousands-sep.sh * gnu/tests/sort/sort-merge-fdlimit.sh * gnu/tests/sort/sort-month.sh * gnu/tests/sort/sort.pl -* gnu/tests/stat/stat-nanoseconds.sh * gnu/tests/tac/tac-2-nonseekable.sh * gnu/tests/tail/end-of-device.sh * gnu/tests/tail/follow-stdin.sh * gnu/tests/tail/inotify-rotate-resources.sh * gnu/tests/tail/symlink.sh -* gnu/tests/touch/obsolescent.sh * gnu/tests/tty/tty-eof.pl From 885e95e5e8cfc8893ce146360af874d763427df8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 28 Nov 2025 20:42:26 +0000 Subject: [PATCH 135/182] chore(deps): update rust crate hostname to v0.4.2 --- Cargo.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a832a8bb3..d8593f579 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1284,13 +1284,13 @@ checksum = "e712f64ec3850b98572bffac52e2c6f282b29fe6c5fa6d42334b30be438d95c1" [[package]] name = "hostname" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56f203cd1c76362b69e3863fd987520ac36cf70a8c92627449b2f64a8cf7d65" +checksum = "617aaa3557aef3810a6369d0a99fac8a080891b68bd9f9812a1eeda0c0730cbd" dependencies = [ "cfg-if", "libc", - "windows-link 0.1.3", + "windows-link 0.2.1", ] [[package]] From 3c0d9511759bdad2ca848bce1f57e7ac38d0cbc0 Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Sat, 29 Nov 2025 07:22:51 +0100 Subject: [PATCH 136/182] Bump iana-time-zone & windows-core iana-time-zone from 0.1.63 to 0.1.64 windows-core from 0.61.2 to 0.62.2 --- Cargo.lock | 42 ++++++++++++++++++------------------------ 1 file changed, 18 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d8593f579..62063777c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -329,7 +329,7 @@ checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ "iana-time-zone", "num-traits", - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -1290,14 +1290,14 @@ checksum = "617aaa3557aef3810a6369d0a99fac8a080891b68bd9f9812a1eeda0c0730cbd" dependencies = [ "cfg-if", "libc", - "windows-link 0.2.1", + "windows-link", ] [[package]] name = "iana-time-zone" -version = "0.1.63" +version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -4420,22 +4420,22 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" -version = "0.61.2" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", - "windows-link 0.1.3", + "windows-link", "windows-result", "windows-strings", ] [[package]] name = "windows-implement" -version = "0.60.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", @@ -4444,21 +4444,15 @@ dependencies = [ [[package]] name = "windows-interface" -version = "0.59.1" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", "syn", ] -[[package]] -name = "windows-link" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - [[package]] name = "windows-link" version = "0.2.1" @@ -4467,20 +4461,20 @@ checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-result" -version = "0.3.4" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-link 0.1.3", + "windows-link", ] [[package]] name = "windows-strings" -version = "0.4.2" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-link 0.1.3", + "windows-link", ] [[package]] @@ -4507,7 +4501,7 @@ version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-link 0.2.1", + "windows-link", ] [[package]] From 2dee0eb6ed88d88bc4481cc8053b02bd2757f5be Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Sat, 29 Nov 2025 07:25:09 +0100 Subject: [PATCH 137/182] deny.toml: remove windows-link from skip list --- deny.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/deny.toml b/deny.toml index 9906813b1..662474b65 100644 --- a/deny.toml +++ b/deny.toml @@ -59,8 +59,6 @@ skip = [ { name = "windows-sys", version = "0.59.0" }, # various crates { name = "windows-sys", version = "0.60.2" }, - # various crates - { name = "windows-link", version = "0.1.3" }, # parking_lot_core { name = "windows-targets", version = "0.52.6" }, # windows-targets From 099a5ddfc815df138a03cbabb03084eeee6a0284 Mon Sep 17 00:00:00 2001 From: mattsu Date: Sat, 29 Nov 2025 20:34:42 +0900 Subject: [PATCH 138/182] test: ensure seq test triggers broken pipe with infinite output Use an infinite sequence in `test_broken_pipe_still_exits_success` instead of finite range (1-5) to guarantee a burst of output immediately after spawn, preventing the process from finishing before stdout closure and avoiding missed broken pipe errors. --- tests/by-util/test_seq.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/by-util/test_seq.rs b/tests/by-util/test_seq.rs index de0ad10d9..d5dd526aa 100644 --- a/tests/by-util/test_seq.rs +++ b/tests/by-util/test_seq.rs @@ -16,7 +16,9 @@ fn test_broken_pipe_still_exits_success() { use std::process::Stdio; let mut child = new_ucmd!() - .args(&["1", "5"]) + // Use an infinite sequence so a burst of output happens immediately after spawn. + // With small output the process can finish before stdout is closed and the Broken pipe never occurs. + .args(&["inf"]) .set_stdout(Stdio::piped()) .run_no_wait(); From 994d07b2112cf38f8ca25d97703dc2a606e822bb Mon Sep 17 00:00:00 2001 From: oech3 <> Date: Sun, 30 Nov 2025 00:44:13 +0900 Subject: [PATCH 139/182] Remove wget dep --- .github/workflows/GnuTests.yml | 2 +- DEVELOPMENT.md | 1 - util/build-gnu.sh | 1 + 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/GnuTests.yml b/.github/workflows/GnuTests.yml index 5986487db..55c570808 100644 --- a/.github/workflows/GnuTests.yml +++ b/.github/workflows/GnuTests.yml @@ -239,7 +239,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 wget automake patch quilt + 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 rustup-init -y --default-toolchain stable - name: Copy the sources to VM run: | diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 2fcd1a7e7..f9636625b 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -291,7 +291,6 @@ brew install \ coreutils \ autoconf \ gettext \ - wget \ texinfo \ xz \ automake \ diff --git a/util/build-gnu.sh b/util/build-gnu.sh index c5bf37267..532e90592 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -131,6 +131,7 @@ if test -f gnu-built; then else # Disable useless checks "${SED}" -i 's|check-texinfo: $(syntax_checks)|check-texinfo:|' doc/local.mk + "${SED}" -i '/^wget.*/d' bootstrap.conf # wget is used to DL po. Remove the dep. ./bootstrap --skip-po # Use CFLAGS for best build time since we discard GNU coreutils CFLAGS="${CFLAGS} -pipe -O0 -s" ./configure --quiet --disable-gcc-warnings --disable-nls --disable-dependency-tracking --disable-bold-man-page-references \ From a917791cc86344cd13eb5a9d2bf5123791894afa Mon Sep 17 00:00:00 2001 From: Christopher Dryden Date: Sat, 29 Nov 2025 15:47:21 +0000 Subject: [PATCH 140/182] Add functionality to show when tests were previously skipped and now failing accurately --- util/compare_test_results.py | 41 ++++++++++++++++++++--- util/test_compare_test_results.py | 55 ++++++++++++++++++++++++------- 2 files changed, 80 insertions(+), 16 deletions(-) diff --git a/util/compare_test_results.py b/util/compare_test_results.py index d5739deae..0f586d5f1 100644 --- a/util/compare_test_results.py +++ b/util/compare_test_results.py @@ -50,14 +50,14 @@ def identify_test_changes(current_flat, reference_flat): reference_flat (dict): Flattened dictionary of reference test results Returns: - tuple: Four lists containing regressions, fixes, newly_skipped, and newly_passing tests + tuple: Five lists containing regressions, fixes, newly_skipped, newly_passing, and newly_failing tests """ # Find regressions (tests that were passing but now failing) regressions = [] for test_path, status in current_flat.items(): if status in ("FAIL", "ERROR"): if test_path in reference_flat: - if reference_flat[test_path] in ("PASS", "SKIP"): + if reference_flat[test_path] == "PASS": regressions.append(test_path) # Find fixes (tests that were failing but now passing) @@ -88,7 +88,17 @@ def identify_test_changes(current_flat, reference_flat): ): newly_passing.append(test_path) - return regressions, fixes, newly_skipped, newly_passing + # Find newly failing tests (were skipped, now failing) + newly_failing = [] + for test_path, status in current_flat.items(): + if ( + status in ("FAIL", "ERROR") + and test_path in reference_flat + and reference_flat[test_path] == "SKIP" + ): + newly_failing.append(test_path) + + return regressions, fixes, newly_skipped, newly_passing, newly_failing def main(): @@ -135,8 +145,8 @@ def main(): reference_flat = flatten_test_results(reference_results) # Identify different categories of test changes - regressions, fixes, newly_skipped, newly_passing = identify_test_changes( - current_flat, reference_flat + regressions, fixes, newly_skipped, newly_passing, newly_failing = ( + identify_test_changes(current_flat, reference_flat) ) # Filter out intermittent issues from regressions @@ -147,6 +157,10 @@ def main(): real_fixes = [f for f in fixes if f not in ignore_list] intermittent_fixes = [f for f in fixes if f in ignore_list] + # Filter out intermittent issues from newly failing + real_newly_failing = [n for n in newly_failing if n not in ignore_list] + intermittent_newly_failing = [n for n in newly_failing if n in ignore_list] + # Print summary stats print(f"Total tests in current run: {len(current_flat)}") print(f"Total tests in reference: {len(reference_flat)}") @@ -156,6 +170,8 @@ def main(): print(f"Intermittent fixes: {len(intermittent_fixes)}") print(f"Newly skipped tests: {len(newly_skipped)}") print(f"Newly passing tests (previously skipped): {len(newly_passing)}") + print(f"Newly failing tests (previously skipped): {len(real_newly_failing)}") + print(f"Intermittent newly failing: {len(intermittent_newly_failing)}") output_lines = [] @@ -206,6 +222,21 @@ def main(): print(f"::notice ::{msg}", file=sys.stderr) output_lines.append(msg) + # Report newly failing tests (were skipped, now failing) + if real_newly_failing: + print("\nNEWLY FAILING TESTS (previously skipped):", file=sys.stderr) + for test in sorted(real_newly_failing): + msg = f"Note: The gnu test {test} was skipped on 'main' but is now failing." + print(f"::warning ::{msg}", file=sys.stderr) + output_lines.append(msg) + + if intermittent_newly_failing: + print("\nINTERMITTENT NEWLY FAILING (ignored):", file=sys.stderr) + for test in sorted(intermittent_newly_failing): + msg = f"Skip an intermittent issue {test} (was skipped on 'main', now failing)" + print(f"::notice ::{msg}", file=sys.stderr) + output_lines.append(msg) + if args.output and output_lines: with open(args.output, "w") as f: for line in output_lines: diff --git a/util/test_compare_test_results.py b/util/test_compare_test_results.py index c3ab4d833..f10557c96 100644 --- a/util/test_compare_test_results.py +++ b/util/test_compare_test_results.py @@ -129,11 +129,11 @@ class TestIdentifyTestChanges(unittest.TestCase): } reference = { "tests/ls/test1": "PASS", - "tests/ls/test2": "SKIP", + "tests/ls/test2": "PASS", "tests/cp/test3": "PASS", "tests/cp/test4": "FAIL", } - regressions, _, _, _ = identify_test_changes(current, reference) + regressions, _, _, _, _ = identify_test_changes(current, reference) self.assertEqual(sorted(regressions), ["tests/ls/test1", "tests/ls/test2"]) def test_fixes(self): @@ -150,7 +150,7 @@ class TestIdentifyTestChanges(unittest.TestCase): "tests/cp/test3": "PASS", "tests/cp/test4": "FAIL", } - _, fixes, _, _ = identify_test_changes(current, reference) + _, fixes, _, _, _ = identify_test_changes(current, reference) self.assertEqual(sorted(fixes), ["tests/ls/test1", "tests/ls/test2"]) def test_newly_skipped(self): @@ -165,7 +165,7 @@ class TestIdentifyTestChanges(unittest.TestCase): "tests/ls/test2": "FAIL", "tests/cp/test3": "PASS", } - _, _, newly_skipped, _ = identify_test_changes(current, reference) + _, _, newly_skipped, _, _ = identify_test_changes(current, reference) self.assertEqual(newly_skipped, ["tests/ls/test1"]) def test_newly_passing(self): @@ -180,7 +180,7 @@ class TestIdentifyTestChanges(unittest.TestCase): "tests/ls/test2": "FAIL", "tests/cp/test3": "SKIP", } - _, _, _, newly_passing = identify_test_changes(current, reference) + _, _, _, newly_passing, _ = identify_test_changes(current, reference) self.assertEqual(newly_passing, ["tests/ls/test1"]) def test_all_categories(self): @@ -191,6 +191,7 @@ class TestIdentifyTestChanges(unittest.TestCase): "tests/cp/test3": "SKIP", # Newly skipped "tests/cp/test4": "PASS", # Newly passing "tests/rm/test5": "PASS", # No change + "tests/rm/test6": "FAIL", # Newly failing } reference = { "tests/ls/test1": "PASS", # Regression @@ -198,14 +199,16 @@ class TestIdentifyTestChanges(unittest.TestCase): "tests/cp/test3": "PASS", # Newly skipped "tests/cp/test4": "SKIP", # Newly passing "tests/rm/test5": "PASS", # No change + "tests/rm/test6": "SKIP", # Newly failing } - regressions, fixes, newly_skipped, newly_passing = identify_test_changes( - current, reference + regressions, fixes, newly_skipped, newly_passing, newly_failing = ( + identify_test_changes(current, reference) ) self.assertEqual(regressions, ["tests/ls/test1"]) self.assertEqual(fixes, ["tests/ls/test2"]) self.assertEqual(newly_skipped, ["tests/cp/test3"]) self.assertEqual(newly_passing, ["tests/cp/test4"]) + self.assertEqual(newly_failing, ["tests/rm/test6"]) def test_new_and_removed_tests(self): """Test handling of tests that are only in one of the datasets.""" @@ -219,13 +222,43 @@ class TestIdentifyTestChanges(unittest.TestCase): "tests/ls/test2": "PASS", "tests/rm/old_test": "FAIL", } - regressions, fixes, newly_skipped, newly_passing = identify_test_changes( - current, reference + regressions, fixes, newly_skipped, newly_passing, newly_failing = ( + identify_test_changes(current, reference) ) self.assertEqual(regressions, ["tests/ls/test2"]) self.assertEqual(fixes, []) self.assertEqual(newly_skipped, []) self.assertEqual(newly_passing, []) + self.assertEqual(newly_failing, []) + + def test_newly_failing(self): + """Test identifying newly failing tests (SKIP -> FAIL).""" + current = { + "tests/ls/test1": "FAIL", + "tests/ls/test2": "ERROR", + "tests/cp/test3": "PASS", + } + reference = { + "tests/ls/test1": "SKIP", + "tests/ls/test2": "SKIP", + "tests/cp/test3": "SKIP", + } + _, _, _, _, newly_failing = identify_test_changes(current, reference) + self.assertEqual(sorted(newly_failing), ["tests/ls/test1", "tests/ls/test2"]) + + def test_skip_to_fail_not_regression(self): + """Test that SKIP -> FAIL is not counted as a regression.""" + current = { + "tests/ls/test1": "FAIL", + "tests/ls/test2": "FAIL", + } + reference = { + "tests/ls/test1": "SKIP", + "tests/ls/test2": "PASS", + } + regressions, _, _, _, newly_failing = identify_test_changes(current, reference) + self.assertEqual(regressions, ["tests/ls/test2"]) + self.assertEqual(newly_failing, ["tests/ls/test1"]) class TestMainFunction(unittest.TestCase): @@ -285,7 +318,7 @@ class TestMainFunction(unittest.TestCase): current_flat = flatten_test_results(self.current_data) reference_flat = flatten_test_results(self.reference_data) - regressions, _, _, _ = identify_test_changes(current_flat, reference_flat) + regressions, _, _, _, _ = identify_test_changes(current_flat, reference_flat) self.assertIn("tests/ls/test2", regressions) @@ -320,7 +353,7 @@ class TestMainFunction(unittest.TestCase): current_flat = flatten_test_results(self.current_data) reference_flat = flatten_test_results(self.reference_data) - _, fixes, _, _ = identify_test_changes(current_flat, reference_flat) + _, fixes, _, _, _ = identify_test_changes(current_flat, reference_flat) # tests/cp/test1 and tests/cp/test2 should be fixed but tests/cp/test1 is in ignore list self.assertIn("tests/cp/test1", fixes) From 8d520239e1e1a1abd71b2b7a085c256a82081dc5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 29 Nov 2025 20:53:51 +0000 Subject: [PATCH 141/182] chore(deps): update vmactions/freebsd-vm action to v1.2.8 --- .github/workflows/freebsd.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/freebsd.yml b/.github/workflows/freebsd.yml index e78607c5b..ee1601f6b 100644 --- a/.github/workflows/freebsd.yml +++ b/.github/workflows/freebsd.yml @@ -43,7 +43,7 @@ jobs: with: disable_annotations: true - name: Prepare, build and test - uses: vmactions/freebsd-vm@v1.2.7 + uses: vmactions/freebsd-vm@v1.2.8 with: usesh: true sync: rsync @@ -139,7 +139,7 @@ jobs: with: disable_annotations: true - name: Prepare, build and test - uses: vmactions/freebsd-vm@v1.2.7 + uses: vmactions/freebsd-vm@v1.2.8 with: usesh: true sync: rsync From 5f31b10d716a83c48b35733b08c2cfb48ef72b6d Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Sun, 30 Nov 2025 16:24:45 +0900 Subject: [PATCH 142/182] why-skip.md: Remove 1 passing root test https://github.com/uutils/coreutils/actions/runs/19789180702/job/56699812412#step:13:47 --- util/why-skip.md | 1 - 1 file changed, 1 deletion(-) diff --git a/util/why-skip.md b/util/why-skip.md index 097fe10b6..48d0b6fc2 100644 --- a/util/why-skip.md +++ b/util/why-skip.md @@ -55,7 +55,6 @@ * tests/df/skip-rootfs.sh = insufficient mount/ext2 support = -* tests/df/problematic-chars.sh * tests/cp/cp-mv-enotsup-xattr.sh = requires controlling input terminal = From 003f21aa58ad8cee96713a2fee6cfca4e4f6ad9b Mon Sep 17 00:00:00 2001 From: mattsu <35655889+mattsu2020@users.noreply.github.com> Date: Mon, 1 Dec 2025 22:09:16 +0900 Subject: [PATCH 143/182] fix(od):fix GNU coreutils test od float.sh (#9534) * feat: add compact float formatting for half and bfloat16 in od Implement trim_float_repr() to remove trailing zeros from float strings while preserving signs and exponents, and pad_float_repr() to align trimmed floats to fixed width. Update format_item_f16() and format_item_bf16() to produce compact output matching GNU od. Add regression tests for float16 and bfloat16 compact printing. * refactor(od): format multiline format! in format_item_bf16 for readability Reformat the format! macro call in the format_item_bf16 function in prn_float.rs to span multiple lines, improving code readability without changing functionality. * fix(od): preserve canonical precision for f16/bf16 float formats Remove trimming of trailing zeros from f16 and bf16 float representations in od output to maintain original precision and align behavior with f32/f64 formatters, ensuring stable output across platforms. Update corresponding tests to reflect the change in expected output. * refactor(od): simplify float padding format and update tests - Remove redundant `width = width` parameter from `format!` macro in `pad_float_repr` - Add "bfloat" to spell-checker ignore list for better test coverage on bf16 format * feat(od): trim trailing zeros in float outputs for GNU compatibility Add `trim_trailing_zeros` function to remove trailing zeros and redundant decimal points from formatted floats, ensuring compact output matching GNU od for f16 and bf16 types. Update `format_item_f16` and `format_item_bf16` to apply trimming before padding. * fix: preserve trailing zeros in F16 and BF16 float formats to match GNU od output Remove the `trim_trailing_zeros` function and update `format_item_f16` and `format_item_bf16` to keep the raw formatted strings without trimming trailing zeros. This ensures consistent column widths and aligns with GNU od behavior for 16-bit float representations, preventing misalignment in output tables. * refactor(od/prn_float): combine multiline format! into single line in format_item_f16 The format! macro call in format_item_f16 was split across multiple lines with newlines. This change consolidates it into a single line for improved code readability and consistency with similar patterns in the file, without altering the function's output or logic. * feat(od): trim trailing zeros in half-precision and bfloat16 float outputs - Add `trim_float_repr` function to remove unnecessary trailing zeros and padding from normalized float strings, leaving exponents unchanged. - Update `format_item_f16` and `format_item_bf16` to apply trimming while maintaining column alignment via re-padding. - Update test expectations to reflect the more compact float representations (e.g., "1" instead of "1.0000000"). * refactor: simplify float trimming condition in prn_float.rs Replace `if let Some(_) = s.find('.')` with `s.find('.').is_some()` in the `trim_float_repr` function to improve code clarity and idiomatic Rust usage while maintaining the same logic for checking decimal presence=black. * Update src/uu/od/src/prn_float.rs Co-authored-by: Daniel Hofstetter --------- Co-authored-by: Daniel Hofstetter --- src/uu/od/src/prn_float.rs | 60 ++++++++++++++++++++++++++++++++++++-- tests/by-util/test_od.rs | 34 ++++++++++++++++++--- 2 files changed, 88 insertions(+), 6 deletions(-) diff --git a/src/uu/od/src/prn_float.rs b/src/uu/od/src/prn_float.rs index 155ce7d07..c93b02d25 100644 --- a/src/uu/od/src/prn_float.rs +++ b/src/uu/od/src/prn_float.rs @@ -37,8 +37,61 @@ pub static FORMAT_ITEM_BF16: FormatterItemInfo = FormatterItemInfo { formatter: FormatWriter::BFloatWriter(format_item_bf16), }; +/// Clean up a normalized float string by removing unnecessary padding and digits. +/// - Strip leading spaces. +/// - Trim trailing zeros after the decimal point (and the dot itself if empty). +/// - Leave the exponent part (e/E...) untouched. +fn trim_float_repr(raw: &str) -> String { + // Drop padding added by `format!` width specification + let mut s = raw.trim_start().to_string(); + + // Keep NaN/Inf representations as-is + let lower = s.to_ascii_lowercase(); + if lower == "nan" || lower == "inf" || lower == "-inf" { + return s; + } + + // Separate exponent from mantissa + let mut exp_part = String::new(); + if let Some(idx) = s.find(['e', 'E']) { + exp_part = s[idx..].to_string(); + s.truncate(idx); + } + + // Trim trailing zeros in mantissa, then remove trailing dot if left alone + if s.contains('.') { + while s.ends_with('0') { + s.pop(); + } + if s.ends_with('.') { + s.pop(); + } + } + + // If everything was trimmed, leave a single zero + if s.is_empty() || s == "-" || s == "+" { + s.push('0'); + } + + s.push_str(&exp_part); + s +} + +/// Pad a floating value to a fixed width for column alignment while keeping +/// the original precision (including trailing zeros). This mirrors the +/// behavior of other float formatters (`f32`, `f64`) and keeps the output +/// stable across platforms. +fn pad_float_repr(raw: &str, width: usize) -> String { + format!("{raw:>width$}") +} + pub fn format_item_f16(f: f64) -> String { - format!(" {}", format_f16(f16::from_f64(f))) + let value = f16::from_f64(f); + let width = FORMAT_ITEM_F16.print_width - 1; + // Format once, trim redundant zeros, then re-pad to the canonical width + let raw = format_f16(value); + let trimmed = trim_float_repr(&raw); + format!(" {}", pad_float_repr(&trimmed, width)) } pub fn format_item_f32(f: f64) -> String { @@ -82,7 +135,10 @@ fn format_f64_exp_precision(f: f64, width: usize, precision: usize) -> String { pub fn format_item_bf16(f: f64) -> String { let bf = bf16::from_f32(f as f32); - format!(" {}", format_binary16_like(f, 15, 8, is_subnormal_bf16(bf))) + let width = FORMAT_ITEM_BF16.print_width - 1; + let raw = format_binary16_like(f64::from(bf), width, 8, is_subnormal_bf16(bf)); + let trimmed = trim_float_repr(&raw); + format!(" {}", pad_float_repr(&trimmed, width)) } fn format_f16(f: f16) -> String { diff --git a/tests/by-util/test_od.rs b/tests/by-util/test_od.rs index 54be34551..fea019e3a 100644 --- a/tests/by-util/test_od.rs +++ b/tests/by-util/test_od.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 abcdefghijklmnopqrstuvwxyz Anone fdbb littl +// spell-checker:ignore abcdefghijklmnopqrstuvwxyz Anone fdbb littl bfloat #[cfg(unix)] use std::io::Read; @@ -197,6 +197,32 @@ fn test_hex32() { .stdout_only(expected_output); } +// Regression: 16-bit IEEE half should print with canonical precision (no spurious digits) +#[test] +fn test_float16_compact() { + let input: [u8; 4] = [0x3c, 0x00, 0x3c, 0x00]; // two times 1.0 in big-endian half + new_ucmd!() + .arg("--endian=big") + .arg("-An") + .arg("-tfH") + .run_piped_stdin(&input[..]) + .success() + .stdout_only(" 1 1\n"); +} + +// Regression: 16-bit bfloat should print with canonical precision (no spurious digits) +#[test] +fn test_bfloat16_compact() { + let input: [u8; 4] = [0x3f, 0x80, 0x3f, 0x80]; // two times 1.0 in big-endian bfloat16 + new_ucmd!() + .arg("--endian=big") + .arg("-An") + .arg("-tfB") + .run_piped_stdin(&input[..]) + .success() + .stdout_only(" 1 1\n"); +} + #[test] fn test_f16() { let input: [u8; 14] = [ @@ -210,7 +236,7 @@ fn test_f16() { ]; // 0x8400 -6.104e-5 let expected_output = unindent( " - 0000000 1.0000000 0 -0 inf + 0000000 1 0 -0 inf 0000010 -inf NaN -6.1035156e-5 0000016 ", @@ -237,7 +263,7 @@ fn test_fh() { ]; // 0x8400 -6.1035156e-5 let expected_output = unindent( " - 0000000 1.0000000 0 -0 inf + 0000000 1 0 -0 inf 0000010 -inf NaN -6.1035156e-5 0000016 ", @@ -264,7 +290,7 @@ fn test_fb() { ]; // -6.1035156e-5 let expected_output = unindent( " - 0000000 1.0000000 0 -0 inf + 0000000 1 0 -0 inf 0000010 -inf NaN -6.1035156e-5 0000016 ", From fa719137ff110af008bf7d5d347c08e45a364f6c Mon Sep 17 00:00:00 2001 From: Maksim Bondarenkov Date: Mon, 1 Dec 2025 16:24:45 +0300 Subject: [PATCH 144/182] uucore: support cygwin requires [libc patch](https://github.com/rust-lang/libc/commit/a3bb40e18a207d53b7eb02fdd21cf97c360aaa37), mio v1.1.0 and [nix patch](nix-rust/nix#\2708). behavior is mostly matched with Linux --- src/uucore/src/lib/features/fs.rs | 2 ++ src/uucore/src/lib/features/fsext.rs | 23 ++++++++----- src/uucore/src/lib/features/signals.rs | 47 ++++++++++++++++++++++++-- 3 files changed, 61 insertions(+), 11 deletions(-) diff --git a/src/uucore/src/lib/features/fs.rs b/src/uucore/src/lib/features/fs.rs index f8d3c0f96..16de054a3 100644 --- a/src/uucore/src/lib/features/fs.rs +++ b/src/uucore/src/lib/features/fs.rs @@ -123,6 +123,7 @@ impl FileInformation { not(target_os = "openbsd"), not(target_os = "illumos"), not(target_os = "solaris"), + not(target_os = "cygwin"), not(target_arch = "aarch64"), not(target_arch = "riscv64"), not(target_arch = "loongarch64"), @@ -140,6 +141,7 @@ impl FileInformation { target_os = "openbsd", target_os = "illumos", target_os = "solaris", + target_os = "cygwin", target_arch = "aarch64", target_arch = "riscv64", target_arch = "loongarch64", diff --git a/src/uucore/src/lib/features/fsext.rs b/src/uucore/src/lib/features/fsext.rs index 4be4d66cf..78dfcceb2 100644 --- a/src/uucore/src/lib/features/fsext.rs +++ b/src/uucore/src/lib/features/fsext.rs @@ -7,9 +7,9 @@ // spell-checker:ignore DATETIME getmntinfo subsecond (fs) cifs smbfs -#[cfg(any(target_os = "linux", target_os = "android"))] +#[cfg(any(target_os = "linux", target_os = "android", target_os = "cygwin"))] const LINUX_MTAB: &str = "/etc/mtab"; -#[cfg(any(target_os = "linux", target_os = "android"))] +#[cfg(any(target_os = "linux", target_os = "android", target_os = "cygwin"))] const LINUX_MOUNTINFO: &str = "/proc/self/mountinfo"; #[cfg(all(unix, not(any(target_os = "aix", target_os = "redox"))))] static MOUNT_OPT_BIND: &str = "bind"; @@ -94,7 +94,8 @@ pub use libc::statfs as StatFs; target_os = "dragonfly", target_os = "illumos", target_os = "solaris", - target_os = "redox" + target_os = "redox", + target_os = "cygwin", ))] pub use libc::statvfs as StatFs; @@ -112,7 +113,8 @@ pub use libc::statfs as statfs_fn; target_os = "illumos", target_os = "solaris", target_os = "dragonfly", - target_os = "redox" + target_os = "redox", + target_os = "cygwin", ))] pub use libc::statvfs as statfs_fn; @@ -189,7 +191,7 @@ pub struct MountInfo { pub dummy: bool, } -#[cfg(any(target_os = "linux", target_os = "android"))] +#[cfg(any(target_os = "linux", target_os = "android", target_os = "cygwin"))] fn replace_special_chars(s: &[u8]) -> Vec { use bstr::ByteSlice; @@ -205,7 +207,7 @@ fn replace_special_chars(s: &[u8]) -> Vec { } impl MountInfo { - #[cfg(any(target_os = "linux", target_os = "android"))] + #[cfg(any(target_os = "linux", target_os = "android", target_os = "cygwin"))] fn new(file_name: &str, raw: &[&[u8]]) -> Option { use std::ffi::OsStr; use std::os::unix::ffi::OsStrExt; @@ -459,9 +461,9 @@ use crate::error::UResult; target_os = "windows" ))] use crate::error::USimpleError; -#[cfg(any(target_os = "linux", target_os = "android"))] +#[cfg(any(target_os = "linux", target_os = "android", target_os = "cygwin"))] use std::fs::File; -#[cfg(any(target_os = "linux", target_os = "android"))] +#[cfg(any(target_os = "linux", target_os = "android", target_os = "cygwin"))] use std::io::{BufRead, BufReader}; #[cfg(any( target_vendor = "apple", @@ -481,7 +483,7 @@ use std::slice; /// Read file system list. pub fn read_fs_list() -> UResult> { - #[cfg(any(target_os = "linux", target_os = "android"))] + #[cfg(any(target_os = "linux", target_os = "android", target_os = "cygwin"))] { let (file_name, f) = File::open(LINUX_MOUNTINFO) .map(|f| (LINUX_MOUNTINFO, f)) @@ -722,6 +724,7 @@ impl FsMeta for StatFs { not(target_os = "solaris"), not(target_os = "redox"), not(target_arch = "s390x"), + not(target_os = "cygwin"), target_pointer_width = "64" ))] return self.f_bsize; @@ -730,6 +733,7 @@ impl FsMeta for StatFs { not(target_os = "freebsd"), not(target_os = "netbsd"), not(target_os = "redox"), + not(target_os = "cygwin"), any( target_arch = "s390x", target_vendor = "apple", @@ -747,6 +751,7 @@ impl FsMeta for StatFs { target_os = "illumos", target_os = "solaris", target_os = "redox", + target_os = "cygwin", all(target_os = "android", target_pointer_width = "64"), ))] return self.f_bsize.try_into().unwrap(); diff --git a/src/uucore/src/lib/features/signals.rs b/src/uucore/src/lib/features/signals.rs index 4e7fe81c9..0bccb2173 100644 --- a/src/uucore/src/lib/features/signals.rs +++ b/src/uucore/src/lib/features/signals.rs @@ -3,8 +3,8 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (vars/api) fcntl setrlimit setitimer rubout pollable sysconf -// spell-checker:ignore (vars/signals) ABRT ALRM CHLD SEGV SIGABRT SIGALRM SIGBUS SIGCHLD SIGCONT SIGDANGER SIGEMT SIGFPE SIGHUP SIGILL SIGINFO SIGINT SIGIO SIGIOT SIGKILL SIGMIGRATE SIGMSG SIGPIPE SIGPRE SIGPROF SIGPWR SIGQUIT SIGSEGV SIGSTOP SIGSYS SIGTALRM SIGTERM SIGTRAP SIGTSTP SIGTHR SIGTTIN SIGTTOU SIGURG SIGUSR SIGVIRT SIGVTALRM SIGWINCH SIGXCPU SIGXFSZ STKFLT PWR THR TSTP TTIN TTOU VIRT VTALRM XCPU XFSZ SIGCLD SIGPOLL SIGWAITING SIGAIOCANCEL SIGLWP SIGFREEZE SIGTHAW SIGCANCEL SIGLOST SIGXRES SIGJVM SIGRTMIN SIGRT SIGRTMAX TALRM AIOCANCEL XRES RTMIN RTMAX +// spell-checker:ignore (vars/api) fcntl setrlimit setitimer rubout pollable sysconf pgrp +// spell-checker:ignore (vars/signals) ABRT ALRM CHLD SEGV SIGABRT SIGALRM SIGBUS SIGCHLD SIGCONT SIGDANGER SIGEMT SIGFPE SIGHUP SIGILL SIGINFO SIGINT SIGIO SIGIOT SIGKILL SIGMIGRATE SIGMSG SIGPIPE SIGPRE SIGPROF SIGPWR SIGQUIT SIGSEGV SIGSTOP SIGSYS SIGTALRM SIGTERM SIGTRAP SIGTSTP SIGTHR SIGTTIN SIGTTOU SIGURG SIGUSR SIGVIRT SIGVTALRM SIGWINCH SIGXCPU SIGXFSZ STKFLT PWR THR TSTP TTIN TTOU VIRT VTALRM XCPU XFSZ SIGCLD SIGPOLL SIGWAITING SIGAIOCANCEL SIGLWP SIGFREEZE SIGTHAW SIGCANCEL SIGLOST SIGXRES SIGJVM SIGRTMIN SIGRT SIGRTMAX TALRM AIOCANCEL XRES RTMIN RTMAX LTOSTOP //! This module provides a way to handle signals in a platform-independent way. //! It provides a way to convert signal names to their corresponding values and vice versa. @@ -346,6 +346,49 @@ pub static ALL_SIGNALS: [&str; 37] = [ "VIRT", "TALRM", ]; +/* + The following signals are defined in Cygwin + https://cygwin.com/cgit/newlib-cygwin/tree/winsup/cygwin/include/cygwin/signal.h + + SIGHUP 1 hangup + SIGINT 2 interrupt + SIGQUIT 3 quit + SIGILL 4 illegal instruction (not reset when caught) + SIGTRAP 5 trace trap (not reset when caught) + SIGABRT 6 used by abort + SIGEMT 7 EMT instruction + SIGFPE 8 floating point exception + SIGKILL 9 kill (cannot be caught or ignored) + SIGBUS 10 bus error + SIGSEGV 11 segmentation violation + SIGSYS 12 bad argument to system call + SIGPIPE 13 write on a pipe with no one to read it + SIGALRM 14 alarm clock + SIGTERM 15 software termination signal from kill + SIGURG 16 urgent condition on IO channel + SIGSTOP 17 sendable stop signal not from tty + SIGTSTP 18 stop signal from tty + SIGCONT 19 continue a stopped process + SIGCHLD 20 to parent on child stop or exit + SIGTTIN 21 to readers pgrp upon background tty read + SIGTTOU 22 like TTIN for output if (tp->t_local<OSTOP) + SIGIO 23 input/output possible signal + SIGXCPU 24 exceeded CPU time limit + SIGXFSZ 25 exceeded file size limit + SIGVTALRM 26 virtual time alarm + SIGPROF 27 profiling time alarm + SIGWINCH 28 window changed + SIGLOST 29 resource lost (eg, record-lock lost) + SIGUSR1 30 user defined signal 1 + SIGUSR2 31 user defined signal 2 +*/ +#[cfg(target_os = "cygwin")] +pub static ALL_SIGNALS: [&str; 32] = [ + "EXIT", "HUP", "INT", "QUIT", "ILL", "TRAP", "ABRT", "EMT", "FPE", "KILL", "BUS", "SEGV", + "SYS", "PIPE", "ALRM", "TERM", "URG", "STOP", "TSTP", "CONT", "CHLD", "TTIN", "TTOU", "IO", + "XCPU", "XFSZ", "VTALRM", "PROF", "WINCH", "PWR", "USR1", "USR2", +]; + /// Returns the signal number for a given signal name or value. pub fn signal_by_name_or_value(signal_name_or_value: &str) -> Option { let signal_name_upcase = signal_name_or_value.to_uppercase(); From a4273c665418b4b99581651af71b7f7aaafcecfd Mon Sep 17 00:00:00 2001 From: Dorian Peron Date: Mon, 24 Nov 2025 23:17:08 +0100 Subject: [PATCH 145/182] test(cksum): Add GNU 9.9 tests to mod gnu_cksum_c --- tests/by-util/test_cksum.rs | 83 +++++++++++++++++++++++++++++-------- 1 file changed, 65 insertions(+), 18 deletions(-) diff --git a/tests/by-util/test_cksum.rs b/tests/by-util/test_cksum.rs index 3d707eb78..4b39627b7 100644 --- a/tests/by-util/test_cksum.rs +++ b/tests/by-util/test_cksum.rs @@ -2458,6 +2458,71 @@ mod gnu_cksum_c { scene } + fn make_scene_with_comment() -> TestScenario { + let scene = make_scene(); + + scene + .fixtures + .append("CHECKSUMS", "# Very important comment\n"); + + scene + } + + fn make_scene_with_invalid_line() -> TestScenario { + let scene = make_scene_with_comment(); + + scene.fixtures.append("CHECKSUMS", "invalid_line\n"); + + scene + } + + #[test] + fn test_tagged_invalid_length() { + let (at, mut ucmd) = at_and_ucmd!(); + + at.write( + "sha2-bad-length.sum", + "SHA2-128 (/dev/null) = 38b060a751ac96384cd9327eb1b1e36a", + ); + + ucmd.arg("--check") + .arg("sha2-bad-length.sum") + .fails() + .stderr_contains("sha2-bad-length.sum: no properly formatted checksum lines found"); + } + + #[test] + #[cfg_attr(not(unix), ignore = "/dev/null is only available on UNIX")] + fn test_untagged_base64_matching_tag() { + let (at, mut ucmd) = at_and_ucmd!(); + + at.write("tag-prefix.sum", "SHA1+++++++++++++++++++++++= /dev/null"); + + ucmd.arg("--check") + .arg("-a") + .arg("sha1") + .arg("tag-prefix.sum") + .fails() + .stderr_contains("WARNING: 1 computed checksum did NOT match"); + } + + #[test] + #[cfg_attr(windows, ignore = "Awkward filename is not supported on windows")] + fn test_awkward_filename() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + + let awkward_file = "abc (f) = abc"; + + at.touch(awkward_file); + + let result = ts.ucmd().arg("-a").arg("sha1").arg(awkward_file).succeeds(); + + at.write_bytes("tag-awkward.sum", result.stdout()); + + ts.ucmd().arg("-c").arg("tag-awkward.sum").succeeds(); + } + #[test] #[ignore = "todo"] fn test_signed_checksums() { @@ -2509,16 +2574,6 @@ mod gnu_cksum_c { .no_output(); } - fn make_scene_with_comment() -> TestScenario { - let scene = make_scene(); - - scene - .fixtures - .append("CHECKSUMS", "# Very important comment\n"); - - scene - } - #[test] fn test_status_with_comment() { let scene = make_scene_with_comment(); @@ -2532,14 +2587,6 @@ mod gnu_cksum_c { .no_output(); } - fn make_scene_with_invalid_line() -> TestScenario { - let scene = make_scene_with_comment(); - - scene.fixtures.append("CHECKSUMS", "invalid_line\n"); - - scene - } - #[test] fn test_check_strict() { let scene = make_scene_with_invalid_line(); From ba5ded050fde2843188a6812450595795d35f571 Mon Sep 17 00:00:00 2001 From: Dorian Peron Date: Fri, 28 Nov 2025 13:17:15 +0100 Subject: [PATCH 146/182] checksum(validation): Rework base64 decoding This commit differentiates Base64 strings that are known to be invalid before decoding (because their length is not a multiple of 4), from Base64 strings that are invalid at decoding (padding is invalid). --- .../src/lib/features/checksum/validate.rs | 68 +++++++++++-------- 1 file changed, 39 insertions(+), 29 deletions(-) diff --git a/src/uucore/src/lib/features/checksum/validate.rs b/src/uucore/src/lib/features/checksum/validate.rs index 6b0595a42..1869d91bf 100644 --- a/src/uucore/src/lib/features/checksum/validate.rs +++ b/src/uucore/src/lib/features/checksum/validate.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 rsplit hexdigit bitlen bytelen invalidchecksum inva idchecksum xffname +// spell-checker:ignore rsplit hexdigit bitlen invalidchecksum inva idchecksum xffname use std::borrow::Cow; use std::ffi::OsStr; @@ -11,7 +11,6 @@ use std::fmt::Display; use std::fs::File; use std::io::{self, BufReader, Read, Write, stdin}; -use data_encoding::BASE64; use os_display::Quotable; use crate::checksum::{AlgoKind, ChecksumError, SizedAlgoKind, digest_reader, unescape_filename}; @@ -467,35 +466,45 @@ fn get_filename_for_output(filename: &OsStr, input_is_stdin: bool) -> String { /// Extract the expected digest from the checksum string fn get_expected_digest_as_hex_string( - line_info: &LineInfo, - len_hint: Option, + checksum: &String, + byte_len_hint: Option, ) -> Option> { - let ck = &line_info.checksum; - - let against_hint = |len| len_hint.is_none_or(|l| l == len); - - if ck.len() % 2 != 0 { + if checksum.len() % 2 != 0 { // If the length of the digest is not a multiple of 2, then it // must be improperly formatted (1 hex digit is 2 characters) return None; } - // If the digest can be decoded as hexadecimal AND its length matches the - // one expected (in case it's given), just go with it. - if ck.as_bytes().iter().all(u8::is_ascii_hexdigit) && against_hint(ck.len()) { - return Some(Cow::Borrowed(ck)); + let checks_hint = |len| byte_len_hint.is_none_or(|hint| hint == len); + + // If the digest can be decoded as hexadecimal AND its byte length matches + // the one expected (in case it's given), just go with it. + if checksum.as_bytes().iter().all(u8::is_ascii_hexdigit) && checks_hint(checksum.len() / 2) { + return Some(checksum.as_str().into()); } - // If hexadecimal digest fails for any reason, interpret the digest as base 64. - BASE64 - .decode(ck.as_bytes()) // Decode the string as encoded base64 - .map(hex::encode) // Encode it back as hexadecimal - .map(Cow::::Owned) - .ok() - .and_then(|s| { - // Check the digest length - if against_hint(s.len()) { Some(s) } else { None } - }) + // If hexadecimal digest fails for any reason, interpret the digest as base + // 64. + + // But first, verify the encoded checksum length, which should be a + // multiple of 4. + if checksum.len() % 4 != 0 { + return None; + } + + // Perform the decoding and be FORGIVING about it, to allow for checksums + // with invalid padding to still be decoded. This is enforced by + // `test_untagged_base64_matching_tag` in `test_cksum.rs` + // + // TODO: Ideally, we should not re-encode the result in hexadecimal, to avoid + // un-necessary computation. + + match base64_simd::forgiving_decode_to_vec(checksum.as_bytes()) { + Ok(buffer) if checks_hint(buffer.len()) => Some(hex::encode(buffer).into()), + // The resulting length is not as expected + Ok(_) => None, + Err(_) => None, + } } /// Returns a reader that reads from the specified file, or from stdin if `filename_to_check` is "-". @@ -691,12 +700,13 @@ fn process_algo_based_line( // If the digest bitlen is known, we can check the format of the expected // checksum with it. let digest_char_length_hint = match (algo_kind, algo_byte_len) { - (AlgoKind::Blake2b, Some(bytelen)) => Some(bytelen * 2), + (AlgoKind::Blake2b, Some(byte_len)) => Some(byte_len), _ => None, }; - let expected_checksum = get_expected_digest_as_hex_string(line_info, digest_char_length_hint) - .ok_or(LineCheckError::ImproperlyFormatted)?; + let expected_checksum = + get_expected_digest_as_hex_string(&line_info.checksum, digest_char_length_hint) + .ok_or(LineCheckError::ImproperlyFormatted)?; let algo = SizedAlgoKind::from_unsized(algo_kind, algo_byte_len)?; @@ -719,7 +729,7 @@ fn process_non_algo_based_line( // Remove the leading asterisk if present - only for the first line filename_to_check = &filename_to_check[1..]; } - let expected_checksum = get_expected_digest_as_hex_string(line_info, None) + let expected_checksum = get_expected_digest_as_hex_string(&line_info.checksum, None) .ok_or(LineCheckError::ImproperlyFormatted)?; // When a specific algorithm name is input, use it and use the provided @@ -1173,7 +1183,7 @@ mod tests { let mut cached_line_format = None; let line_info = LineInfo::parse(&line, &mut cached_line_format).unwrap(); - let result = get_expected_digest_as_hex_string(&line_info, None); + let result = get_expected_digest_as_hex_string(&line_info.checksum, None); assert_eq!( result.unwrap(), @@ -1188,7 +1198,7 @@ mod tests { let mut cached_line_format = None; let line_info = LineInfo::parse(&line, &mut cached_line_format).unwrap(); - let result = get_expected_digest_as_hex_string(&line_info, None); + let result = get_expected_digest_as_hex_string(&line_info.checksum, None); assert!(result.is_none()); } From ddc703c7480d3f2c5f7840e74703bd166324d2f7 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Tue, 2 Dec 2025 15:12:07 +0900 Subject: [PATCH 147/182] why-skip.md: Let spell-checker:ignore a comment --- util/why-skip.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/util/why-skip.md b/util/why-skip.md index 48d0b6fc2..f179a58bb 100644 --- a/util/why-skip.md +++ b/util/why-skip.md @@ -1,5 +1,4 @@ -# spell-checker:ignore epipe readdir restorecon SIGALRM capget bigtime rootfs enotsup - + = skipped test: breakpoint not hit = * tests/tail-2/inotify-race2.sh * tail-2/inotify-race.sh From 4fe86586cec1d48870103e6d39832ba2ee6202bf Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 2 Dec 2025 17:48:24 +0000 Subject: [PATCH 148/182] chore(deps): update rust crate ctor to v0.6.2 --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 62063777c..04a008d46 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -789,9 +789,9 @@ dependencies = [ [[package]] name = "ctor" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ffc71fcdcdb40d6f087edddf7f8f1f8f79e6cf922f555a9ee8779752d4819bd" +checksum = "eb230974aaf0aca4d71665bed0aca156cf43b764fcb9583b69c6c3e686f35e72" dependencies = [ "ctor-proc-macro", "dtor", From f90846824fdc62edb17edddff5bea95445f8764d Mon Sep 17 00:00:00 2001 From: Christopher Dryden Date: Wed, 3 Dec 2025 00:12:22 +0000 Subject: [PATCH 149/182] Splitting parser feature into multiple subfeatures to reduce dependency bloat --- fuzz/Cargo.lock | 65 +++++++++++------------ src/uu/dd/Cargo.toml | 2 +- src/uu/df/Cargo.toml | 2 +- src/uu/du/Cargo.toml | 3 +- src/uu/head/Cargo.toml | 2 +- src/uu/ls/Cargo.toml | 3 +- src/uu/od/Cargo.toml | 2 +- src/uu/shred/Cargo.toml | 2 +- src/uu/sort/Cargo.toml | 4 +- src/uu/split/Cargo.toml | 2 +- src/uu/stdbuf/Cargo.toml | 2 +- src/uu/tail/Cargo.toml | 2 +- src/uu/truncate/Cargo.toml | 2 +- src/uucore/Cargo.toml | 13 +++-- src/uucore/src/lib/features.rs | 7 ++- src/uucore/src/lib/features/parser/mod.rs | 5 ++ src/uucore/src/lib/lib.rs | 7 ++- 17 files changed, 72 insertions(+), 53 deletions(-) diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index f224e0437..989bce43f 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -196,9 +196,9 @@ checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" [[package]] name = "cc" -version = "1.2.46" +version = "1.2.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36" +checksum = "c481bdbf0ed3b892f6f806287d72acd515b352a4ec27a208489b8c1bc839633a" dependencies = [ "find-msvc-tools", "jobserver", @@ -231,18 +231,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.51" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" +checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.51" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" +checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" dependencies = [ "anstream", "anstyle", @@ -325,9 +325,9 @@ dependencies = [ [[package]] name = "crc" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" dependencies = [ "crc-catalog", ] @@ -875,9 +875,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.82" +version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" dependencies = [ "once_cell", "wasm-bindgen", @@ -894,9 +894,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.177" +version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" [[package]] name = "libfuzzer-sys" @@ -928,9 +928,9 @@ checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "md-5" @@ -1083,9 +1083,9 @@ checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" [[package]] name = "parse_datetime" -version = "0.13.2" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4955561bc7aa4c40afcfd2a8c34297b13164ae9ac3b30ac348737befdc98e4c" +checksum = "acea383beda9652270f3c9678d83aa58cbfc16880343cae0c0c8c7d6c0974132" dependencies = [ "jiff", "num-traits", @@ -1417,9 +1417,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.110" +version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" dependencies = [ "proc-macro2", "quote", @@ -1737,7 +1737,6 @@ dependencies = [ "bigdecimal", "blake2b_simd", "blake3", - "bstr", "clap", "crc-fast", "data-encoding", @@ -1846,9 +1845,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" dependencies = [ "cfg-if", "once_cell", @@ -1859,9 +1858,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1869,9 +1868,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" dependencies = [ "bumpalo", "proc-macro2", @@ -1882,9 +1881,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" dependencies = [ "unicode-ident", ] @@ -2051,9 +2050,9 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.13" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" dependencies = [ "memchr", ] @@ -2107,18 +2106,18 @@ checksum = "9b3a41ce106832b4da1c065baa4c31cf640cf965fa1483816402b7f6b96f0a64" [[package]] name = "zerocopy" -version = "0.8.27" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.27" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" dependencies = [ "proc-macro2", "quote", diff --git a/src/uu/dd/Cargo.toml b/src/uu/dd/Cargo.toml index 4633bc06b..d1ac79fb5 100644 --- a/src/uu/dd/Cargo.toml +++ b/src/uu/dd/Cargo.toml @@ -23,7 +23,7 @@ gcd = { workspace = true } libc = { workspace = true } uucore = { workspace = true, features = [ "format", - "parser", + "parser-size", "quoting-style", "fs", ] } diff --git a/src/uu/df/Cargo.toml b/src/uu/df/Cargo.toml index 8f0d7d082..93017870d 100644 --- a/src/uu/df/Cargo.toml +++ b/src/uu/df/Cargo.toml @@ -19,7 +19,7 @@ path = "src/df.rs" [dependencies] clap = { workspace = true } -uucore = { workspace = true, features = ["libc", "fsext", "parser", "fs"] } +uucore = { workspace = true, features = ["libc", "fsext", "parser-size", "fs"] } unicode-width = { workspace = true } thiserror = { workspace = true } fluent = { workspace = true } diff --git a/src/uu/du/Cargo.toml b/src/uu/du/Cargo.toml index 87898811f..1241746e7 100644 --- a/src/uu/du/Cargo.toml +++ b/src/uu/du/Cargo.toml @@ -24,7 +24,8 @@ clap = { workspace = true } uucore = { workspace = true, features = [ "format", "fsext", - "parser", + "parser-size", + "parser-glob", "time", "safe-traversal", ] } diff --git a/src/uu/head/Cargo.toml b/src/uu/head/Cargo.toml index 9ee84db3d..2c0e18c1a 100644 --- a/src/uu/head/Cargo.toml +++ b/src/uu/head/Cargo.toml @@ -22,7 +22,7 @@ clap = { workspace = true } memchr = { workspace = true } thiserror = { workspace = true } uucore = { workspace = true, features = [ - "parser", + "parser-size", "ringbuffer", "lines", "fs", diff --git a/src/uu/ls/Cargo.toml b/src/uu/ls/Cargo.toml index 5fab67614..e6cd07fa4 100644 --- a/src/uu/ls/Cargo.toml +++ b/src/uu/ls/Cargo.toml @@ -36,7 +36,8 @@ uucore = { workspace = true, features = [ "fs", "fsext", "fsxattr", - "parser", + "parser-size", + "parser-glob", "quoting-style", "time", "version-cmp", diff --git a/src/uu/od/Cargo.toml b/src/uu/od/Cargo.toml index 97676a86e..13d12a413 100644 --- a/src/uu/od/Cargo.toml +++ b/src/uu/od/Cargo.toml @@ -21,7 +21,7 @@ path = "src/od.rs" byteorder = { workspace = true } clap = { workspace = true } half = { workspace = true } -uucore = { workspace = true, features = ["parser"] } +uucore = { workspace = true, features = ["parser-size"] } fluent = { workspace = true } libc.workspace = true diff --git a/src/uu/shred/Cargo.toml b/src/uu/shred/Cargo.toml index 9f5294d3b..59f0fb6c2 100644 --- a/src/uu/shred/Cargo.toml +++ b/src/uu/shred/Cargo.toml @@ -20,7 +20,7 @@ path = "src/shred.rs" [dependencies] clap = { workspace = true } rand = { workspace = true } -uucore = { workspace = true, features = ["parser"] } +uucore = { workspace = true, features = ["parser-size"] } libc = { workspace = true } fluent = { workspace = true } diff --git a/src/uu/sort/Cargo.toml b/src/uu/sort/Cargo.toml index c1b4c0708..e65f70d5a 100644 --- a/src/uu/sort/Cargo.toml +++ b/src/uu/sort/Cargo.toml @@ -34,7 +34,7 @@ self_cell = { workspace = true } tempfile = { workspace = true } thiserror = { workspace = true } unicode-width = { workspace = true } -uucore = { workspace = true, features = ["fs", "parser", "version-cmp"] } +uucore = { workspace = true, features = ["fs", "parser-size", "version-cmp"] } fluent = { workspace = true } nix = { workspace = true } @@ -44,7 +44,7 @@ tempfile = { workspace = true } uucore = { workspace = true, features = [ "benchmark", "fs", - "parser", + "parser-size", "version-cmp", "i18n-collator", ] } diff --git a/src/uu/split/Cargo.toml b/src/uu/split/Cargo.toml index d6cf871ac..2c51bb780 100644 --- a/src/uu/split/Cargo.toml +++ b/src/uu/split/Cargo.toml @@ -20,7 +20,7 @@ path = "src/split.rs" [dependencies] clap = { workspace = true } memchr = { workspace = true } -uucore = { workspace = true, features = ["fs", "parser"] } +uucore = { workspace = true, features = ["fs", "parser-size"] } thiserror = { workspace = true } fluent = { workspace = true } diff --git a/src/uu/stdbuf/Cargo.toml b/src/uu/stdbuf/Cargo.toml index ce64792b7..cb5445026 100644 --- a/src/uu/stdbuf/Cargo.toml +++ b/src/uu/stdbuf/Cargo.toml @@ -22,7 +22,7 @@ path = "src/stdbuf.rs" clap = { workspace = true } libstdbuf = { package = "uu_stdbuf_libstdbuf", version = "0.4.0", path = "src/libstdbuf" } tempfile = { workspace = true } -uucore = { workspace = true, features = ["parser"] } +uucore = { workspace = true, features = ["parser-size"] } thiserror = { workspace = true } fluent = { workspace = true } diff --git a/src/uu/tail/Cargo.toml b/src/uu/tail/Cargo.toml index bf8952759..7d7b57a74 100644 --- a/src/uu/tail/Cargo.toml +++ b/src/uu/tail/Cargo.toml @@ -23,7 +23,7 @@ clap = { workspace = true } libc = { workspace = true } memchr = { workspace = true } notify = { workspace = true } -uucore = { workspace = true, features = ["fs", "parser"] } +uucore = { workspace = true, features = ["fs", "parser-size"] } same-file = { workspace = true } fluent = { workspace = true } diff --git a/src/uu/truncate/Cargo.toml b/src/uu/truncate/Cargo.toml index 29eeaccec..07ab63e6d 100644 --- a/src/uu/truncate/Cargo.toml +++ b/src/uu/truncate/Cargo.toml @@ -19,7 +19,7 @@ path = "src/truncate.rs" [dependencies] clap = { workspace = true } -uucore = { workspace = true, features = ["parser"] } +uucore = { workspace = true, features = ["parser-size"] } fluent = { workspace = true } [[bin]] diff --git a/src/uucore/Cargo.toml b/src/uucore/Cargo.toml index 46b2f9daa..4e056e6cb 100644 --- a/src/uucore/Cargo.toml +++ b/src/uucore/Cargo.toml @@ -22,7 +22,7 @@ workspace = true path = "src/lib/lib.rs" [dependencies] -bstr = { workspace = true } +bstr = { workspace = true, optional = true } chrono = { workspace = true, optional = true } clap = { workspace = true } uucore_procs = { workspace = true } @@ -102,7 +102,7 @@ xattr = { workspace = true, optional = true } tempfile = { workspace = true } [target.'cfg(target_os = "linux")'.dependencies] -procfs = { workspace = true } +procfs = { workspace = true, optional = true } [target.'cfg(target_os = "windows")'.dependencies] winapi-util = { workspace = true, optional = true } @@ -129,7 +129,7 @@ entries = ["libc"] extendedbigdecimal = ["bigdecimal", "num-traits"] fast-inc = [] fs = ["dunce", "libc", "winapi-util", "windows-sys"] -fsext = ["libc", "windows-sys"] +fsext = ["libc", "windows-sys", "bstr"] fsxattr = ["xattr"] hardware = [] lines = [] @@ -138,7 +138,7 @@ format = [ "bigdecimal", "extendedbigdecimal", "itertools", - "parser", + "parser-num", "num-traits", "quoting-style", ] @@ -149,7 +149,10 @@ i18n-decimal = ["i18n-common", "icu_decimal", "icu_provider"] mode = ["libc"] perms = ["entries", "libc", "walkdir"] buf-copy = [] -parser = ["extendedbigdecimal", "glob", "num-traits"] +parser-num = ["extendedbigdecimal", "num-traits"] +parser-size = ["parser-num", "procfs"] +parser-glob = ["glob"] +parser = ["parser-num", "parser-size", "parser-glob"] pipes = [] process = ["libc"] proc-info = ["tty", "walkdir"] diff --git a/src/uucore/src/lib/features.rs b/src/uucore/src/lib/features.rs index 6d239642a..548f7f2bc 100644 --- a/src/uucore/src/lib/features.rs +++ b/src/uucore/src/lib/features.rs @@ -32,7 +32,12 @@ pub mod fsext; pub mod i18n; #[cfg(feature = "lines")] pub mod lines; -#[cfg(feature = "parser")] +#[cfg(any( + feature = "parser", + feature = "parser-num", + feature = "parser-size", + feature = "parser-glob" +))] pub mod parser; #[cfg(feature = "quoting-style")] pub mod quoting_style; diff --git a/src/uucore/src/lib/features/parser/mod.rs b/src/uucore/src/lib/features/parser/mod.rs index 800fe6e8c..d2fc27721 100644 --- a/src/uucore/src/lib/features/parser/mod.rs +++ b/src/uucore/src/lib/features/parser/mod.rs @@ -4,8 +4,13 @@ // file that was distributed with this source code. // spell-checker:ignore extendedbigdecimal +#[cfg(any(feature = "parser", feature = "parser-num"))] pub mod num_parser; +#[cfg(any(feature = "parser", feature = "parser-glob"))] pub mod parse_glob; +#[cfg(any(feature = "parser", feature = "parser-size"))] pub mod parse_size; +#[cfg(any(feature = "parser", feature = "parser-num"))] pub mod parse_time; +#[cfg(any(feature = "parser", feature = "parser-num"))] pub mod shortcut_value_parser; diff --git a/src/uucore/src/lib/lib.rs b/src/uucore/src/lib/lib.rs index 5459c5d54..e4e871a5f 100644 --- a/src/uucore/src/lib/lib.rs +++ b/src/uucore/src/lib/lib.rs @@ -60,7 +60,12 @@ pub use crate::features::hardware; pub use crate::features::i18n; #[cfg(feature = "lines")] pub use crate::features::lines; -#[cfg(feature = "parser")] +#[cfg(any( + feature = "parser", + feature = "parser-num", + feature = "parser-size", + feature = "parser-glob" +))] pub use crate::features::parser; #[cfg(feature = "quoting-style")] pub use crate::features::quoting_style; From ffa5aa4cfdf54a29c715dc7e378d5701b2d69f7d Mon Sep 17 00:00:00 2001 From: Christopher Dryden Date: Wed, 3 Dec 2025 05:11:50 +0000 Subject: [PATCH 150/182] Making wild a windows only dependency and gating unit-prefix --- fuzz/Cargo.lock | 64 +++++++++++++++++++-------------------- src/uucore/Cargo.toml | 5 +-- src/uucore/src/lib/lib.rs | 3 ++ 3 files changed, 38 insertions(+), 34 deletions(-) diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index f224e0437..f41dbfc19 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -196,9 +196,9 @@ checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" [[package]] name = "cc" -version = "1.2.46" +version = "1.2.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36" +checksum = "c481bdbf0ed3b892f6f806287d72acd515b352a4ec27a208489b8c1bc839633a" dependencies = [ "find-msvc-tools", "jobserver", @@ -231,18 +231,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.51" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" +checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.51" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" +checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" dependencies = [ "anstream", "anstyle", @@ -325,9 +325,9 @@ dependencies = [ [[package]] name = "crc" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" dependencies = [ "crc-catalog", ] @@ -875,9 +875,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.82" +version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" dependencies = [ "once_cell", "wasm-bindgen", @@ -894,9 +894,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.177" +version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" [[package]] name = "libfuzzer-sys" @@ -928,9 +928,9 @@ checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "md-5" @@ -1083,9 +1083,9 @@ checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" [[package]] name = "parse_datetime" -version = "0.13.2" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4955561bc7aa4c40afcfd2a8c34297b13164ae9ac3b30ac348737befdc98e4c" +checksum = "acea383beda9652270f3c9678d83aa58cbfc16880343cae0c0c8c7d6c0974132" dependencies = [ "jiff", "num-traits", @@ -1417,9 +1417,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.110" +version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" dependencies = [ "proc-macro2", "quote", @@ -1846,9 +1846,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" dependencies = [ "cfg-if", "once_cell", @@ -1859,9 +1859,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1869,9 +1869,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" dependencies = [ "bumpalo", "proc-macro2", @@ -1882,9 +1882,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" dependencies = [ "unicode-ident", ] @@ -2051,9 +2051,9 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.13" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" dependencies = [ "memchr", ] @@ -2107,18 +2107,18 @@ checksum = "9b3a41ce106832b4da1c065baa4c31cf640cf965fa1483816402b7f6b96f0a64" [[package]] name = "zerocopy" -version = "0.8.27" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.27" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" dependencies = [ "proc-macro2", "quote", diff --git a/src/uucore/Cargo.toml b/src/uucore/Cargo.toml index 46b2f9daa..14338d43e 100644 --- a/src/uucore/Cargo.toml +++ b/src/uucore/Cargo.toml @@ -26,11 +26,10 @@ bstr = { workspace = true } chrono = { workspace = true, optional = true } clap = { workspace = true } uucore_procs = { workspace = true } -unit-prefix = { workspace = true } +unit-prefix = { workspace = true, optional = true } phf = { workspace = true } dns-lookup = { workspace = true, optional = true } dunce = { version = "1.0.4", optional = true } -wild = "2.2.1" glob = { workspace = true, optional = true } itertools = { workspace = true, optional = true } jiff = { workspace = true, optional = true, features = [ @@ -105,6 +104,7 @@ tempfile = { workspace = true } procfs = { workspace = true } [target.'cfg(target_os = "windows")'.dependencies] +wild = "2.2.1" winapi-util = { workspace = true, optional = true } windows-sys = { workspace = true, optional = true, default-features = false, features = [ "Wdk_System_SystemInformation", @@ -141,6 +141,7 @@ format = [ "parser", "num-traits", "quoting-style", + "unit-prefix", ] i18n-all = ["i18n-collator", "i18n-decimal"] i18n-common = ["icu_locale"] diff --git a/src/uucore/src/lib/lib.rs b/src/uucore/src/lib/lib.rs index 5459c5d54..f1bce1884 100644 --- a/src/uucore/src/lib/lib.rs +++ b/src/uucore/src/lib/lib.rs @@ -323,7 +323,10 @@ pub fn set_utility_is_second_arg() { // args_os() can be expensive to call, it copies all of argv before iterating. // So if we want only the first arg or so it's overkill. We cache it. +#[cfg(windows)] static ARGV: LazyLock> = LazyLock::new(|| wild::args_os().collect()); +#[cfg(not(windows))] +static ARGV: LazyLock> = LazyLock::new(|| std::env::args_os().collect()); static UTIL_NAME: LazyLock = LazyLock::new(|| { let base_index = usize::from(get_utility_is_second_arg()); From b1513da5f5859e42a7afcaf482211922e0f6121b Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Thu, 4 Dec 2025 00:13:24 +0900 Subject: [PATCH 151/182] du: Alias -A --apparent-size --- src/uu/du/src/du.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/uu/du/src/du.rs b/src/uu/du/src/du.rs index 4c29d07d3..522252a8b 100644 --- a/src/uu/du/src/du.rs +++ b/src/uu/du/src/du.rs @@ -1257,6 +1257,7 @@ pub fn uu_app() -> Command { ) .arg( Arg::new(options::APPARENT_SIZE) + .short('A') .long(options::APPARENT_SIZE) .help(translate!("du-help-apparent-size")) .action(ArgAction::SetTrue), From 1ef13d5486a9296a7ac2eed49b8c6feb6d75d97d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 4 Dec 2025 21:38:51 +0000 Subject: [PATCH 152/182] chore(deps): update rust crate ctor to v0.6.3 --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 04a008d46..2b142f5a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -789,9 +789,9 @@ dependencies = [ [[package]] name = "ctor" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb230974aaf0aca4d71665bed0aca156cf43b764fcb9583b69c6c3e686f35e72" +checksum = "424e0138278faeb2b401f174ad17e715c829512d74f3d1e81eb43365c2e0590e" dependencies = [ "ctor-proc-macro", "dtor", From 2a248de1fb67193dcfdf9db122f16d0aba72ff56 Mon Sep 17 00:00:00 2001 From: Dorian Peron Date: Tue, 25 Nov 2025 01:07:38 +0100 Subject: [PATCH 153/182] checksum: Introduce a DigestOutput type... ... to prevent a preemptive computation of the hex encoding. --- src/uucore/Cargo.toml | 3 +- .../src/lib/features/checksum/compute.rs | 62 +++++-------- src/uucore/src/lib/features/checksum/mod.rs | 16 +--- .../src/lib/features/checksum/validate.rs | 11 +-- src/uucore/src/lib/features/sum.rs | 91 ++++++++++++++----- 5 files changed, 104 insertions(+), 79 deletions(-) diff --git a/src/uucore/Cargo.toml b/src/uucore/Cargo.toml index 46b2f9daa..0f38bed05 100644 --- a/src/uucore/Cargo.toml +++ b/src/uucore/Cargo.toml @@ -123,7 +123,7 @@ default = [] # * non-default features backup-control = [] colors = [] -checksum = ["data-encoding", "quoting-style", "sum"] +checksum = ["quoting-style", "sum", "base64-simd"] encoding = ["data-encoding", "data-encoding-macro", "z85", "base64-simd"] entries = ["libc"] extendedbigdecimal = ["bigdecimal", "num-traits"] @@ -171,6 +171,7 @@ sum = [ "blake3", "sm3", "crc-fast", + "data-encoding", ] update-control = ["parser"] utf8 = [] diff --git a/src/uucore/src/lib/features/checksum/compute.rs b/src/uucore/src/lib/features/checksum/compute.rs index e91c54166..471e8c66a 100644 --- a/src/uucore/src/lib/features/checksum/compute.rs +++ b/src/uucore/src/lib/features/checksum/compute.rs @@ -13,7 +13,8 @@ use std::path::Path; use crate::checksum::{ChecksumError, SizedAlgoKind, digest_reader, escape_filename}; use crate::error::{FromIo, UResult, USimpleError}; use crate::line_ending::LineEnding; -use crate::{encoding, show, translate}; +use crate::sum::DigestOutput; +use crate::{show, translate}; /// Use the same buffer size as GNU when reading a file to create a checksum /// from it: 32 KiB. @@ -139,10 +140,11 @@ pub fn figure_out_output_format( fn print_legacy_checksum( options: &ChecksumComputeOptions, filename: &OsStr, - sum: &str, + sum: &DigestOutput, size: usize, ) -> UResult<()> { debug_assert!(options.algo_kind.is_legacy()); + debug_assert!(matches!(sum, DigestOutput::U16(_) | DigestOutput::Crc(_))); let (escaped_filename, prefix) = if options.line_ending == LineEnding::Nul { (filename.to_string_lossy().to_string(), "") @@ -150,28 +152,24 @@ fn print_legacy_checksum( escape_filename(filename) }; - print!("{prefix}"); - // Print the sum - match options.algo_kind { - SizedAlgoKind::Sysv => print!( - "{} {}", - sum.parse::().unwrap(), + match (options.algo_kind, sum) { + (SizedAlgoKind::Sysv, DigestOutput::U16(sum)) => print!( + "{prefix}{sum} {}", size.div_ceil(options.algo_kind.bitlen()), ), - SizedAlgoKind::Bsd => { + (SizedAlgoKind::Bsd, DigestOutput::U16(sum)) => { // The BSD checksum output is 5 digit integer let bsd_width = 5; print!( - "{:0bsd_width$} {:bsd_width$}", - sum.parse::().unwrap(), + "{prefix}{sum:0bsd_width$} {:bsd_width$}", size.div_ceil(options.algo_kind.bitlen()), ); } - SizedAlgoKind::Crc | SizedAlgoKind::Crc32b => { - print!("{sum} {size}"); + (SizedAlgoKind::Crc | SizedAlgoKind::Crc32b, DigestOutput::Crc(sum)) => { + print!("{prefix}{sum} {size}"); } - _ => unreachable!("Not a legacy algorithm"), + (algo, output) => unreachable!("Bug: Invalid legacy checksum ({algo:?}, {output:?})"), } // Print the filename after a space if not stdin @@ -284,49 +282,39 @@ where let mut digest = options.algo_kind.create_digest(); - let (sum_hex, sz) = digest_reader( - &mut digest, - &mut file, - options.binary, - options.algo_kind.bitlen(), - ) - .map_err_context(|| translate!("cksum-error-failed-to-read-input"))?; + let (digest_output, sz) = digest_reader(&mut digest, &mut file, options.binary) + .map_err_context(|| translate!("cksum-error-failed-to-read-input"))?; // Encodes the sum if df is Base64, leaves as-is otherwise. - let encode_sum = |sum: String, df: DigestFormat| { + let encode_sum = |sum: DigestOutput, df: DigestFormat| { if df.is_base64() { - encoding::for_cksum::BASE64.encode(&hex::decode(sum).unwrap()) + sum.to_base64() } else { - sum + sum.to_hex() } }; match options.output_format { OutputFormat::Raw => { - let bytes = match options.algo_kind { - SizedAlgoKind::Crc | SizedAlgoKind::Crc32b => { - sum_hex.parse::().unwrap().to_be_bytes().to_vec() - } - SizedAlgoKind::Sysv | SizedAlgoKind::Bsd => { - sum_hex.parse::().unwrap().to_be_bytes().to_vec() - } - _ => hex::decode(sum_hex).unwrap(), - }; // Cannot handle multiple files anyway, output immediately. - io::stdout().write_all(&bytes)?; + digest_output.write_raw(io::stdout())?; return Ok(()); } OutputFormat::Legacy => { - print_legacy_checksum(&options, filename, &sum_hex, sz)?; + print_legacy_checksum(&options, filename, &digest_output, sz)?; } OutputFormat::Tagged(digest_format) => { - print_tagged_checksum(&options, filename, &encode_sum(sum_hex, digest_format))?; + print_tagged_checksum( + &options, + filename, + &encode_sum(digest_output, digest_format)?, + )?; } OutputFormat::Untagged(digest_format, reading_mode) => { print_untagged_checksum( &options, filename, - &encode_sum(sum_hex, digest_format), + &encode_sum(digest_output, digest_format)?, reading_mode, )?; } diff --git a/src/uucore/src/lib/features/checksum/mod.rs b/src/uucore/src/lib/features/checksum/mod.rs index 87c8836fd..5339f833f 100644 --- a/src/uucore/src/lib/features/checksum/mod.rs +++ b/src/uucore/src/lib/features/checksum/mod.rs @@ -15,8 +15,8 @@ use thiserror::Error; use crate::error::{UError, UResult}; use crate::show_error; use crate::sum::{ - Blake2b, Blake3, Bsd, CRC32B, Crc, Digest, DigestWriter, Md5, Sha1, Sha3_224, Sha3_256, - Sha3_384, Sha3_512, Sha224, Sha256, Sha384, Sha512, Shake128, Shake256, Sm3, SysV, + Blake2b, Blake3, Bsd, CRC32B, Crc, Digest, DigestOutput, DigestWriter, Md5, Sha1, Sha3_224, + Sha3_256, Sha3_384, Sha3_512, Sha224, Sha256, Sha384, Sha512, Shake128, Shake256, Sm3, SysV, }; pub mod compute; @@ -420,8 +420,7 @@ pub fn digest_reader( digest: &mut Box, reader: &mut T, binary: bool, - output_bits: usize, -) -> io::Result<(String, usize)> { +) -> io::Result<(DigestOutput, usize)> { digest.reset(); // Read bytes from `reader` and write those bytes to `digest`. @@ -440,14 +439,7 @@ pub fn digest_reader( let output_size = std::io::copy(reader, &mut digest_writer)? as usize; digest_writer.finalize(); - if digest.output_bits() > 0 { - Ok((digest.result_str(), output_size)) - } else { - // Assume it's SHAKE. result_str() doesn't work with shake (as of 8/30/2016) - let mut bytes = vec![0; output_bits.div_ceil(8)]; - digest.hash_finalize(&mut bytes); - Ok((hex::encode(bytes), output_size)) - } + Ok((digest.result(), output_size)) } /// Calculates the length of the digest. diff --git a/src/uucore/src/lib/features/checksum/validate.rs b/src/uucore/src/lib/features/checksum/validate.rs index 1869d91bf..06bfd6634 100644 --- a/src/uucore/src/lib/features/checksum/validate.rs +++ b/src/uucore/src/lib/features/checksum/validate.rs @@ -660,16 +660,11 @@ fn compute_and_check_digest_from_file( // TODO: improve function signature to use ReadingMode instead of binary bool // Set binary to false because --binary is not supported with --check - let (calculated_checksum, _) = digest_reader( - &mut digest, - &mut file_reader, - /* binary */ false, - algo.bitlen(), - ) - .unwrap(); + let (calculated_checksum, _) = + digest_reader(&mut digest, &mut file_reader, /* binary */ false).unwrap(); // Do the checksum validation - let checksum_correct = expected_checksum == calculated_checksum; + let checksum_correct = expected_checksum == calculated_checksum.to_hex()?; print_file_report( std::io::stdout(), filename, diff --git a/src/uucore/src/lib/features/sum.rs b/src/uucore/src/lib/features/sum.rs index e517a03fc..66fb752ab 100644 --- a/src/uucore/src/lib/features/sum.rs +++ b/src/uucore/src/lib/features/sum.rs @@ -12,12 +12,52 @@ //! [`DigestWriter`] struct provides a wrapper around [`Digest`] that //! implements the [`Write`] trait, for use in situations where calling //! [`write`] would be useful. -use std::io::Write; -use hex::encode; +use std::io::{self, Write}; + +use data_encoding::BASE64; + #[cfg(windows)] use memchr::memmem; +use crate::error::{UResult, USimpleError}; + +/// Represents the output of a checksum computation. +#[derive(Debug)] +pub enum DigestOutput { + /// Varying-size output + Vec(Vec), + /// Legacy output for Crc and Crc32B modes + Crc(u32), + /// Legacy output for Sysv and BSD modes + U16(u16), +} + +impl DigestOutput { + pub fn write_raw(&self, mut w: impl std::io::Write) -> io::Result<()> { + match self { + Self::Vec(buf) => w.write_all(buf), + // For legacy outputs, print them in big endian + Self::Crc(n) => w.write_all(&n.to_be_bytes()), + Self::U16(n) => w.write_all(&n.to_be_bytes()), + } + } + + pub fn to_hex(&self) -> UResult { + match self { + Self::Vec(buf) => Ok(hex::encode(buf)), + _ => Err(USimpleError::new(1, "Legacy output cannot be encoded")), + } + } + + pub fn to_base64(&self) -> UResult { + match self { + Self::Vec(buf) => Ok(BASE64.encode(buf)), + _ => Err(USimpleError::new(1, "Legacy output cannot be encoded")), + } + } +} + pub trait Digest { fn new() -> Self where @@ -29,10 +69,11 @@ pub trait Digest { fn output_bytes(&self) -> usize { self.output_bits().div_ceil(8) } - fn result_str(&mut self) -> String { + + fn result(&mut self) -> DigestOutput { let mut buf: Vec = vec![0; self.output_bytes()]; self.hash_finalize(&mut buf); - encode(buf) + DigestOutput::Vec(buf) } } @@ -167,10 +208,12 @@ impl Digest for Crc { out.copy_from_slice(&self.digest.finalize().to_ne_bytes()); } - fn result_str(&mut self) -> String { + fn result(&mut self) -> DigestOutput { let mut out: [u8; 8] = [0; 8]; self.hash_finalize(&mut out); - u64::from_ne_bytes(out).to_string() + + let x = u64::from_ne_bytes(out); + DigestOutput::Crc((x & (u32::MAX as u64)) as u32) } fn reset(&mut self) { @@ -214,10 +257,10 @@ impl Digest for CRC32B { 32 } - fn result_str(&mut self) -> String { + fn result(&mut self) -> DigestOutput { let mut out = [0; 4]; self.hash_finalize(&mut out); - format!("{}", u32::from_be_bytes(out)) + DigestOutput::Crc(u32::from_be_bytes(out)) } } @@ -240,10 +283,10 @@ impl Digest for Bsd { out.copy_from_slice(&self.state.to_ne_bytes()); } - fn result_str(&mut self) -> String { - let mut _out: Vec = vec![0; 2]; + fn result(&mut self) -> DigestOutput { + let mut _out = [0; 2]; self.hash_finalize(&mut _out); - format!("{}", self.state) + DigestOutput::U16(self.state) } fn reset(&mut self) { @@ -275,10 +318,10 @@ impl Digest for SysV { out.copy_from_slice(&(self.state as u16).to_ne_bytes()); } - fn result_str(&mut self) -> String { - let mut _out: Vec = vec![0; 2]; + fn result(&mut self) -> DigestOutput { + let mut _out = [0; 2]; self.hash_finalize(&mut _out); - format!("{}", self.state) + DigestOutput::U16((self.state & (u16::MAX as u32)) as u16) } fn reset(&mut self) { @@ -292,7 +335,7 @@ impl Digest for SysV { // Implements the Digest trait for sha2 / sha3 algorithms with fixed output macro_rules! impl_digest_common { - ($algo_type: ty, $size: expr) => { + ($algo_type: ty, $size: literal) => { impl Digest for $algo_type { fn new() -> Self { Self(Default::default()) @@ -319,7 +362,7 @@ macro_rules! impl_digest_common { // Implements the Digest trait for sha2 / sha3 algorithms with variable output macro_rules! impl_digest_shake { - ($algo_type: ty) => { + ($algo_type: ty, $output_bits: literal) => { impl Digest for $algo_type { fn new() -> Self { Self(Default::default()) @@ -338,7 +381,13 @@ macro_rules! impl_digest_shake { } fn output_bits(&self) -> usize { - 0 + $output_bits + } + + fn result(&mut self) -> DigestOutput { + let mut bytes = vec![0; self.output_bits().div_ceil(8)]; + self.hash_finalize(&mut bytes); + DigestOutput::Vec(bytes) } } }; @@ -368,8 +417,8 @@ impl_digest_common!(Sha3_512, 512); pub struct Shake128(sha3::Shake128); pub struct Shake256(sha3::Shake256); -impl_digest_shake!(Shake128); -impl_digest_shake!(Shake256); +impl_digest_shake!(Shake128, 256); +impl_digest_shake!(Shake256, 512); /// A struct that writes to a digest. /// @@ -501,14 +550,14 @@ mod tests { writer_crlf.write_all(b"\r").unwrap(); writer_crlf.write_all(b"\n").unwrap(); writer_crlf.finalize(); - let result_crlf = digest.result_str(); + let result_crlf = digest.result(); // We expect "\r\n" to be replaced with "\n" in text mode on Windows. let mut digest = Box::new(Md5::new()) as Box; let mut writer_lf = DigestWriter::new(&mut digest, false); writer_lf.write_all(b"\n").unwrap(); writer_lf.finalize(); - let result_lf = digest.result_str(); + let result_lf = digest.result(); assert_eq!(result_crlf, result_lf); } From 896cef58adbd89b9b7fdafec45321e7f7877d1ca Mon Sep 17 00:00:00 2001 From: Dorian Peron Date: Fri, 28 Nov 2025 17:08:07 +0100 Subject: [PATCH 154/182] checksum(validate): Simplify and optimize LineFormat::validate_checksum_format --- .../src/lib/features/checksum/validate.rs | 45 +++++++++++-------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/src/uucore/src/lib/features/checksum/validate.rs b/src/uucore/src/lib/features/checksum/validate.rs index 06bfd6634..5c7063e48 100644 --- a/src/uucore/src/lib/features/checksum/validate.rs +++ b/src/uucore/src/lib/features/checksum/validate.rs @@ -371,19 +371,32 @@ impl LineFormat { return None; } - let mut parts = checksum.splitn(2, |&b| b == b'='); - let main = parts.next().unwrap(); // Always exists since checksum isn't empty - let padding = parts.next().unwrap_or_default(); // Empty if no '=' + let mut is_base64 = false; - if main.is_empty() - || !main - .iter() - .all(|&b| b.is_ascii_alphanumeric() || b == b'+' || b == b'/') - { - return None; + for index in 0..checksum.len() { + match checksum[index..] { + // ASCII alphanumeric + [b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9', ..] => (), + // Base64 special character + [b'+' | b'/', ..] => is_base64 = true, + // Base64 end of string padding + [b'='] | [b'=', b'='] | [b'=', b'=', b'='] => { + is_base64 = true; + break; + } + // Any other character means the checksum is wrong + _ => return None, + } } - if padding.len() > 2 || padding.iter().any(|&b| b != b'=') { + // If base64 characters were encountered, make sure the checksum has a + // length multiple of 4. + // + // This check is not enough because it may allow base64-encoded + // checksums that are fully alphanumeric. Another check happens later + // when we are provided with a length hint to detect ambiguous + // base64-encoded checksums. + if is_base64 && checksum.len() % 4 != 0 { return None; } @@ -1174,11 +1187,9 @@ mod tests { #[test] fn test_get_expected_digest() { - let line = OsString::from("SHA256 (empty) = 47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU="); - let mut cached_line_format = None; - let line_info = LineInfo::parse(&line, &mut cached_line_format).unwrap(); + let ck = "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=".to_owned(); - let result = get_expected_digest_as_hex_string(&line_info.checksum, None); + let result = get_expected_digest_as_hex_string(&ck, None); assert_eq!( result.unwrap(), @@ -1189,11 +1200,9 @@ mod tests { #[test] fn test_get_expected_checksum_invalid() { // The line misses a '=' at the end to be valid base64 - let line = OsString::from("SHA256 (empty) = 47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU"); - let mut cached_line_format = None; - let line_info = LineInfo::parse(&line, &mut cached_line_format).unwrap(); + let ck = "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU".to_owned(); - let result = get_expected_digest_as_hex_string(&line_info.checksum, None); + let result = get_expected_digest_as_hex_string(&ck, None); assert!(result.is_none()); } From d3733db1195e69b17c4cc549a515e2413e851b27 Mon Sep 17 00:00:00 2001 From: Dorian Peron Date: Sat, 29 Nov 2025 03:37:45 +0100 Subject: [PATCH 155/182] checksum(validate): Check calculated checksum against raw expected to avoid decoding base64 and directly re-encoding it in hexadecimal --- .../src/lib/features/checksum/validate.rs | 79 +++++++++---------- 1 file changed, 38 insertions(+), 41 deletions(-) diff --git a/src/uucore/src/lib/features/checksum/validate.rs b/src/uucore/src/lib/features/checksum/validate.rs index 5c7063e48..c36baca87 100644 --- a/src/uucore/src/lib/features/checksum/validate.rs +++ b/src/uucore/src/lib/features/checksum/validate.rs @@ -5,7 +5,6 @@ // spell-checker:ignore rsplit hexdigit bitlen invalidchecksum inva idchecksum xffname -use std::borrow::Cow; use std::ffi::OsStr; use std::fmt::Display; use std::fs::File; @@ -16,6 +15,7 @@ use os_display::Quotable; use crate::checksum::{AlgoKind, ChecksumError, SizedAlgoKind, digest_reader, unescape_filename}; use crate::error::{FromIo, UError, UResult, USimpleError}; use crate::quoting_style::{QuotingStyle, locale_aware_escape_name}; +use crate::sum::DigestOutput; use crate::{ os_str_as_bytes, os_str_from_bytes, read_os_string_lines, show, show_error, show_warning_caps, util_name, @@ -477,47 +477,45 @@ fn get_filename_for_output(filename: &OsStr, input_is_stdin: bool) -> String { .to_string() } -/// Extract the expected digest from the checksum string -fn get_expected_digest_as_hex_string( - checksum: &String, - byte_len_hint: Option, -) -> Option> { +/// Extract the expected digest from the checksum string and decode it +fn get_raw_expected_digest(checksum: &str, byte_len_hint: Option) -> Option> { + // If the length of the digest is not a multiple of 2, then it must be + // improperly formatted (1 byte is 2 hex digits, and base64 strings should + // always be a multiple of 4). if checksum.len() % 2 != 0 { - // If the length of the digest is not a multiple of 2, then it - // must be improperly formatted (1 hex digit is 2 characters) return None; } let checks_hint = |len| byte_len_hint.is_none_or(|hint| hint == len); - // If the digest can be decoded as hexadecimal AND its byte length matches - // the one expected (in case it's given), just go with it. - if checksum.as_bytes().iter().all(u8::is_ascii_hexdigit) && checks_hint(checksum.len() / 2) { - return Some(checksum.as_str().into()); + // If the length of the string matches the one to be expected (in case it's + // given) AND the digest can be decoded as hexadecimal, just go with it. + if checks_hint(checksum.len() / 2) { + if let Ok(raw_ck) = hex::decode(checksum) { + return Some(raw_ck); + } } - // If hexadecimal digest fails for any reason, interpret the digest as base - // 64. + // If the checksum cannot be decoded as hexadecimal, interpret it as Base64 + // instead. // But first, verify the encoded checksum length, which should be a // multiple of 4. + // + // It is important to check it before trying to decode, because the + // forgiving mode of decoding will ignore if padding characters '=' are + // MISSING, but to match GNU's behavior, we must reject it. if checksum.len() % 4 != 0 { return None; } // Perform the decoding and be FORGIVING about it, to allow for checksums - // with invalid padding to still be decoded. This is enforced by + // with INVALID padding to still be decoded. This is enforced by // `test_untagged_base64_matching_tag` in `test_cksum.rs` - // - // TODO: Ideally, we should not re-encode the result in hexadecimal, to avoid - // un-necessary computation. - match base64_simd::forgiving_decode_to_vec(checksum.as_bytes()) { - Ok(buffer) if checks_hint(buffer.len()) => Some(hex::encode(buffer).into()), - // The resulting length is not as expected - Ok(_) => None, - Err(_) => None, - } + base64_simd::forgiving_decode_to_vec(checksum.as_bytes()) + .ok() + .filter(|raw| checks_hint(raw.len())) } /// Returns a reader that reads from the specified file, or from stdin if `filename_to_check` is "-". @@ -657,7 +655,7 @@ fn identify_algo_name_and_length( /// the expected one. fn compute_and_check_digest_from_file( filename: &[u8], - expected_checksum: &str, + expected_checksum: &[u8], algo: SizedAlgoKind, opts: ChecksumValidateOptions, ) -> Result<(), LineCheckError> { @@ -677,7 +675,11 @@ fn compute_and_check_digest_from_file( digest_reader(&mut digest, &mut file_reader, /* binary */ false).unwrap(); // Do the checksum validation - let checksum_correct = expected_checksum == calculated_checksum.to_hex()?; + let checksum_correct = match calculated_checksum { + DigestOutput::Vec(data) => data == expected_checksum, + DigestOutput::Crc(n) => n.to_be_bytes() == expected_checksum, + DigestOutput::U16(n) => n.to_be_bytes() == expected_checksum, + }; print_file_report( std::io::stdout(), filename, @@ -712,9 +714,8 @@ fn process_algo_based_line( _ => None, }; - let expected_checksum = - get_expected_digest_as_hex_string(&line_info.checksum, digest_char_length_hint) - .ok_or(LineCheckError::ImproperlyFormatted)?; + let expected_checksum = get_raw_expected_digest(&line_info.checksum, digest_char_length_hint) + .ok_or(LineCheckError::ImproperlyFormatted)?; let algo = SizedAlgoKind::from_unsized(algo_kind, algo_byte_len)?; @@ -737,22 +738,17 @@ fn process_non_algo_based_line( // Remove the leading asterisk if present - only for the first line filename_to_check = &filename_to_check[1..]; } - let expected_checksum = get_expected_digest_as_hex_string(&line_info.checksum, None) + let expected_checksum = get_raw_expected_digest(&line_info.checksum, None) .ok_or(LineCheckError::ImproperlyFormatted)?; // When a specific algorithm name is input, use it and use the provided // bits except when dealing with blake2b, sha2 and sha3, where we will // detect the length. let (algo_kind, algo_byte_len) = match cli_algo_kind { - AlgoKind::Blake2b => { - // division by 2 converts the length of the Blake2b checksum from - // hexadecimal characters to bytes, as each byte is represented by - // two hexadecimal characters. - (AlgoKind::Blake2b, Some(expected_checksum.len() / 2)) - } + AlgoKind::Blake2b => (AlgoKind::Blake2b, Some(expected_checksum.len())), algo @ (AlgoKind::Sha2 | AlgoKind::Sha3) => { - // multiplication by 4 to get the number of bits - (algo, Some(expected_checksum.len() * 4)) + // multiplication by 8 to get the number of bits + (algo, Some(expected_checksum.len() * 8)) } _ => (cli_algo_kind, cli_algo_length), }; @@ -1189,11 +1185,12 @@ mod tests { fn test_get_expected_digest() { let ck = "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=".to_owned(); - let result = get_expected_digest_as_hex_string(&ck, None); + let result = get_raw_expected_digest(&ck, None); assert_eq!( result.unwrap(), - "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + hex::decode(b"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") + .unwrap() ); } @@ -1202,7 +1199,7 @@ mod tests { // The line misses a '=' at the end to be valid base64 let ck = "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU".to_owned(); - let result = get_expected_digest_as_hex_string(&ck, None); + let result = get_raw_expected_digest(&ck, None); assert!(result.is_none()); } From 88d1cf7384790e09494502300c203f67e934ad99 Mon Sep 17 00:00:00 2001 From: Dorian Peron Date: Mon, 1 Dec 2025 02:13:08 +0100 Subject: [PATCH 156/182] test(cksum): Add test for ignore-missing standard input --- tests/by-util/test_cksum.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/by-util/test_cksum.rs b/tests/by-util/test_cksum.rs index 4b39627b7..d4685d619 100644 --- a/tests/by-util/test_cksum.rs +++ b/tests/by-util/test_cksum.rs @@ -2755,6 +2755,20 @@ mod gnu_cksum_c { .stderr_contains("CHECKSUMS-missing: no file was verified"); } + #[test] + fn test_ignore_missing_stdin() { + let scene = make_scene_with_checksum_missing(); + + scene + .ucmd() + .arg("--ignore-missing") + .arg("--check") + .pipe_in_fixture("CHECKSUMS-missing") + .fails() + .no_stdout() + .stderr_contains("'standard input': no file was verified"); + } + #[test] fn test_status_and_warn() { let scene = make_scene_with_checksum_missing(); From 6009543fc35874936848bddcbcf8a2ce44d40c28 Mon Sep 17 00:00:00 2001 From: Dorian Peron Date: Mon, 1 Dec 2025 01:58:19 +0100 Subject: [PATCH 157/182] checksum(validate): Remove called-once simple functions, fix standard-input filename print --- .../src/lib/features/checksum/validate.rs | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/uucore/src/lib/features/checksum/validate.rs b/src/uucore/src/lib/features/checksum/validate.rs index c36baca87..e91a07cae 100644 --- a/src/uucore/src/lib/features/checksum/validate.rs +++ b/src/uucore/src/lib/features/checksum/validate.rs @@ -170,10 +170,16 @@ fn print_cksum_report(res: &ChecksumResult) { /// Print a "no properly formatted lines" message in stderr #[inline] -fn log_no_properly_formatted(filename: String) { +fn log_no_properly_formatted(filename: impl Display) { show_error!("{filename}: no properly formatted checksum lines found"); } +/// Print a "no file was verified" message in stderr +#[inline] +fn log_no_file_verified(filename: impl Display) { + show_error!("{filename}: no file was verified"); +} + /// Represents the different outcomes that can happen to a file /// that is being checked. #[derive(Debug, Clone, Copy)] @@ -467,16 +473,6 @@ impl LineInfo { } } -fn get_filename_for_output(filename: &OsStr, input_is_stdin: bool) -> String { - if input_is_stdin { - "standard input" - } else { - filename.to_str().unwrap() - } - .maybe_quote() - .to_string() -} - /// Extract the expected digest from the checksum string and decode it fn get_raw_expected_digest(checksum: &str, byte_len_hint: Option) -> Option> { // If the length of the digest is not a multiple of 2, then it must be @@ -882,11 +878,19 @@ fn process_checksum_file( } } + let filename_display = || { + if input_is_stdin { + "standard input".maybe_quote() + } else { + filename_input.maybe_quote() + } + }; + // not a single line correctly formatted found // return an error if res.total_properly_formatted() == 0 { if opts.verbose.over_status() { - log_no_properly_formatted(get_filename_for_output(filename_input, input_is_stdin)); + log_no_properly_formatted(filename_display()); } return Err(FileCheckError::Failed); } @@ -900,11 +904,7 @@ fn process_checksum_file( // we have only bad format // and we had ignore-missing if opts.verbose.over_status() { - eprintln!( - "{}: {}: no file was verified", - util_name(), - filename_input.maybe_quote(), - ); + log_no_file_verified(filename_display()); } return Err(FileCheckError::Failed); } From ad13266153acdf4320e6c23a5521f40bdbf9d1f0 Mon Sep 17 00:00:00 2001 From: Dorian Peron Date: Mon, 1 Dec 2025 02:59:12 +0100 Subject: [PATCH 158/182] l10n(uucore::checksum): Implement l10n for English + French --- src/uu/cksum/locales/en-US.ftl | 4 -- src/uu/cksum/locales/fr-FR.ftl | 4 -- src/uucore/locales/en-US.ftl | 19 ++++++ src/uucore/locales/fr-FR.ftl | 19 ++++++ .../src/lib/features/checksum/compute.rs | 6 +- .../src/lib/features/checksum/validate.rs | 59 +++++++++++-------- 6 files changed, 73 insertions(+), 38 deletions(-) diff --git a/src/uu/cksum/locales/en-US.ftl b/src/uu/cksum/locales/en-US.ftl index 4a49caebd..834cd77b0 100644 --- a/src/uu/cksum/locales/en-US.ftl +++ b/src/uu/cksum/locales/en-US.ftl @@ -28,7 +28,3 @@ cksum-help-quiet = don't print OK for each successfully verified file cksum-help-ignore-missing = don't fail or report status for missing files cksum-help-zero = end each output line with NUL, not newline, and disable file name escaping cksum-help-debug = print CPU hardware capability detection info used by cksum - -# Error messages -cksum-error-is-directory = { $file }: Is a directory -cksum-error-failed-to-read-input = failed to read input diff --git a/src/uu/cksum/locales/fr-FR.ftl b/src/uu/cksum/locales/fr-FR.ftl index 686584696..01136f606 100644 --- a/src/uu/cksum/locales/fr-FR.ftl +++ b/src/uu/cksum/locales/fr-FR.ftl @@ -28,7 +28,3 @@ cksum-help-quiet = ne pas afficher OK pour chaque fichier vérifié avec succès cksum-help-ignore-missing = ne pas échouer ou signaler le statut pour les fichiers manquants cksum-help-zero = terminer chaque ligne de sortie avec NUL, pas un saut de ligne, et désactiver l'échappement des noms de fichiers cksum-help-debug = afficher les informations de débogage sur la détection de la prise en charge matérielle du processeur - -# Messages d'erreur -cksum-error-is-directory = { $file } : Est un répertoire -cksum-error-failed-to-read-input = échec de la lecture de l'entrée diff --git a/src/uucore/locales/en-US.ftl b/src/uucore/locales/en-US.ftl index 09fb45783..384e4a83d 100644 --- a/src/uucore/locales/en-US.ftl +++ b/src/uucore/locales/en-US.ftl @@ -29,6 +29,7 @@ error-io = I/O error error-permission-denied = Permission denied error-file-not-found = No such file or directory error-invalid-argument = Invalid argument +error-is-a-directory = { $file }: Is a directory # Common actions action-copying = copying @@ -54,3 +55,21 @@ safe-traversal-error-unlink-failed = failed to unlink '{ $path }': { $source } safe-traversal-error-invalid-fd = invalid file descriptor safe-traversal-current-directory = safe-traversal-directory = + +# checksum-related messages +checksum-no-properly-formatted = { $checksum_file }: no properly formatted checksum lines found +checksum-no-file-verified = { $checksum_file }: no file was verified +checksum-error-failed-to-read-input = failed to read input +checksum-bad-format = { $count -> + [1] { $count } line is improperly formatted + *[other] { $count } lines are improperly formatted +} +checksum-failed-cksum = { $count -> + [1] { $count } computed checksum did NOT match + *[other] { $count } computed checksums did NOT match +} +checksum-failed-open-file = { $count -> + [1] { $count } listed file could not be read + *[other] { $count } listed files could not be read +} +checksum-error-algo-bad-format = { $file }: { $line }: improperly formatted { $algo } checksum line diff --git a/src/uucore/locales/fr-FR.ftl b/src/uucore/locales/fr-FR.ftl index a8a344688..4c844e9b1 100644 --- a/src/uucore/locales/fr-FR.ftl +++ b/src/uucore/locales/fr-FR.ftl @@ -29,6 +29,7 @@ error-io = Erreur E/S error-permission-denied = Permission refusée error-file-not-found = Aucun fichier ou répertoire de ce type error-invalid-argument = Argument invalide +error-is-a-directory = { $file }: Est un répertoire # Actions communes action-copying = copie @@ -54,3 +55,21 @@ safe-traversal-error-unlink-failed = échec de la suppression de '{ $path }' : { safe-traversal-error-invalid-fd = descripteur de fichier invalide safe-traversal-current-directory = safe-traversal-directory = + +# Messages relatifs au module checksum +checksum-no-properly-formatted = { $checksum_file }: aucune ligne correctement formattée n'a été trouvée +checksum-no-file-verified = { $checksum_file }: aucun fichier n'a été vérifié +checksum-error-failed-to-read-input = échec de la lecture de l'entrée +checksum-bad-format = { $count -> + [1] { $count } ligne invalide + *[other] { $count } lignes invalides +} +checksum-failed-cksum = { $count -> + [1] { $count } somme de hachage ne correspond PAS + *[other] { $count } sommes de hachage ne correspondent PAS +} +checksum-failed-open-file = { $count -> + [1] { $count } fichier passé n'a pas pu être lu + *[other] { $count } fichiers passés n'ont pas pu être lu +} +checksum-error-algo-bad-format = { $file }: { $line }: ligne invalide pour { $algo } diff --git a/src/uucore/src/lib/features/checksum/compute.rs b/src/uucore/src/lib/features/checksum/compute.rs index 471e8c66a..956c1e4c1 100644 --- a/src/uucore/src/lib/features/checksum/compute.rs +++ b/src/uucore/src/lib/features/checksum/compute.rs @@ -255,9 +255,7 @@ where if filepath.is_dir() { show!(USimpleError::new( 1, - // TODO: Rework translation, which is broken since this code moved to uucore - // translate!("cksum-error-is-directory", "file" => filepath.display()) - format!("{}: Is a directory", filepath.display()) + translate!("error-is-a-directory", "file" => filepath.display()) )); continue; } @@ -283,7 +281,7 @@ where let mut digest = options.algo_kind.create_digest(); let (digest_output, sz) = digest_reader(&mut digest, &mut file, options.binary) - .map_err_context(|| translate!("cksum-error-failed-to-read-input"))?; + .map_err_context(|| translate!("checksum-error-failed-to-read-input"))?; // Encodes the sum if df is Base64, leaves as-is otherwise. let encode_sum = |sum: DigestOutput, df: DigestFormat| { diff --git a/src/uucore/src/lib/features/checksum/validate.rs b/src/uucore/src/lib/features/checksum/validate.rs index e91a07cae..ae18e6202 100644 --- a/src/uucore/src/lib/features/checksum/validate.rs +++ b/src/uucore/src/lib/features/checksum/validate.rs @@ -18,7 +18,7 @@ use crate::quoting_style::{QuotingStyle, locale_aware_escape_name}; use crate::sum::DigestOutput; use crate::{ os_str_as_bytes, os_str_from_bytes, read_os_string_lines, show, show_error, show_warning_caps, - util_name, + translate, }; /// To what level should checksum validation print logging info. @@ -147,37 +147,45 @@ impl From for FileCheckError { } } -#[allow(clippy::comparison_chain)] fn print_cksum_report(res: &ChecksumResult) { - if res.bad_format == 1 { - show_warning_caps!("{} line is improperly formatted", res.bad_format); - } else if res.bad_format > 1 { - show_warning_caps!("{} lines are improperly formatted", res.bad_format); + if res.bad_format > 0 { + show_warning_caps!( + "{}", + translate!("checksum-bad-format", "count" => res.bad_format) + ); } - if res.failed_cksum == 1 { - show_warning_caps!("{} computed checksum did NOT match", res.failed_cksum); - } else if res.failed_cksum > 1 { - show_warning_caps!("{} computed checksums did NOT match", res.failed_cksum); + if res.failed_cksum > 0 { + show_warning_caps!( + "{}", + translate!("checksum-failed-cksum", "count" => res.failed_cksum) + ); } - if res.failed_open_file == 1 { - show_warning_caps!("{} listed file could not be read", res.failed_open_file); - } else if res.failed_open_file > 1 { - show_warning_caps!("{} listed files could not be read", res.failed_open_file); + if res.failed_open_file > 0 { + show_warning_caps!( + "{}", + translate!("checksum-failed-open-file", "count" => res.failed_open_file) + ); } } /// Print a "no properly formatted lines" message in stderr #[inline] fn log_no_properly_formatted(filename: impl Display) { - show_error!("{filename}: no properly formatted checksum lines found"); + show_error!( + "{}", + translate!("checksum-no-properly-formatted", "checksum_file" => filename) + ); } /// Print a "no file was verified" message in stderr #[inline] fn log_no_file_verified(filename: impl Display) { - show_error!("{filename}: no file was verified"); + show_error!( + "{}", + translate!("checksum-no-file-verified", "checksum_file" => filename) + ); } /// Represents the different outcomes that can happen to a file @@ -576,17 +584,18 @@ fn get_input_file(filename: &OsStr) -> UResult> { match File::open(filename) { Ok(f) => { if f.metadata()?.is_dir() { - Err( - io::Error::other(format!("{}: Is a directory", filename.to_string_lossy())) - .into(), + Err(io::Error::other( + translate!("error-is-a-directory", "file" => filename.to_string_lossy()), ) + .into()) } else { Ok(Box::new(f)) } } Err(_) => Err(io::Error::other(format!( - "{}: No such file or directory", - filename.to_string_lossy() + "{}: {}", + filename.to_string_lossy(), + translate!("error-file-not-found") )) .into()), } @@ -864,11 +873,9 @@ fn process_checksum_file( } else { "Unknown algorithm" }; - eprintln!( - "{}: {}: {}: improperly formatted {algo} checksum line", - util_name(), - filename_input.maybe_quote(), - i + 1, + show_error!( + "{}", + translate!("checksum-error-algo-bad-format", "file" => filename_input.maybe_quote(), "line" => i + 1, "algo" => algo) ); } } From 1dca6469f23f20ea9529635e2d9af65817e9d556 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Fri, 5 Dec 2025 18:12:17 +0900 Subject: [PATCH 159/182] installation.md: Add MSYS2 Cygwin package --- docs/src/installation.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/src/installation.md b/docs/src/installation.md index 537504cc5..8ff0f004e 100644 --- a/docs/src/installation.md +++ b/docs/src/installation.md @@ -172,7 +172,9 @@ scoop install uutils-coreutils ### MSYS2 -[MSYS2 package](https://packages.msys2.org/base/mingw-w64-uutils-coreutils) +[MSYS2 package (Windows native)](https://packages.msys2.org/base/mingw-w64-uutils-coreutils) + +[MSYS2 package (Cygwin)](https://packages.msys2.org/base/uutils-coreutils) ## Alternative installers From 5708bcd4105e83b82afd3dd95404ed8c66a35e46 Mon Sep 17 00:00:00 2001 From: oech3 <> Date: Fri, 5 Dec 2025 22:18:54 +0900 Subject: [PATCH 160/182] Remove Makefile.toml --- Makefile.toml | 386 -------------------------------------------------- 1 file changed, 386 deletions(-) delete mode 100644 Makefile.toml diff --git a/Makefile.toml b/Makefile.toml deleted file mode 100644 index 84698df5f..000000000 --- a/Makefile.toml +++ /dev/null @@ -1,386 +0,0 @@ -# spell-checker:ignore (cargo-make) duckscript - -[config] -min_version = "0.26.2" -default_to_workspace = false -init_task = "_init_task" - -[config.modify_core_tasks] -namespace = "core" - -### initialization - -### * note: the task executed from 'init_task' ignores dependencies; workaround is to run a secondary task via 'run_task' - -[tasks._init_task] -# dependencies are unavailable -# * delegate (via 'run_task') to "real" initialization task ('_init') with full capabilities -private = true -run_task = "_init" - -[tasks._init] -private = true -dependencies = ["_init-vars"] - -[tasks._init-vars] -private = true -script_runner = "@duckscript" -script = [''' -# reset build/test flags -set_env CARGO_MAKE_CARGO_BUILD_TEST_FLAGS "" -# determine features -env_features = get_env CARGO_FEATURES -if is_empty "${env_features}" - env_features = get_env FEATURES -end_if -if is_empty "${env_features}" - if eq "${CARGO_MAKE_RUST_TARGET_OS}" "macos" - features = set "unix" - else - if eq "${CARGO_MAKE_RUST_TARGET_OS}" "linux" - features = set "unix" - else - if eq "${CARGO_MAKE_RUST_TARGET_OS}" "windows" - features = set "windows" - end_if - end_if - end_if -end_if -if is_empty "${features}" - features = set "${env_features}" -else - if not is_empty "${env_features}" - features = set "${features},${env_features}" - end_if -end_if -# set build flags from features -if not is_empty "${features}" - set_env CARGO_MAKE_VAR_BUILD_TEST_FEATURES "${features}" - set_env CARGO_MAKE_CARGO_BUILD_TEST_FLAGS "--features ${features}" -end_if -# determine show-utils helper script -show_utils = set "util/show-utils.sh" -if eq "${CARGO_MAKE_RUST_TARGET_OS}" "windows" - show_utils = set "util/show-utils.BAT" -end_if -set_env CARGO_MAKE_VAR_SHOW_UTILS "${show_utils}" -# rebuild CARGO_MAKE_TASK_ARGS for various targets -args = set ${CARGO_MAKE_TASK_ARGS} -# * rebuild for 'features' target -args_features = replace ${args} ";" "," -set_env CARGO_MAKE_TASK_BUILD_FEATURES_ARGS "${args_features}" -# * rebuild for 'examples' target -args_examples = replace ${args} ";" " --example " -if is_empty "${args_examples}" - args_examples = set "--examples" -end_if -set_env CARGO_MAKE_TASK_BUILD_EXAMPLES_ARGS "${args_examples}" -# * rebuild for 'utils' target -args_utils_list = split "${args}" ";" -for arg in "${args_utils_list}" - if not is_empty "${arg}" - if not starts_with "${arg}" "uu_" - arg = set "uu_${arg}" - end_if - args_utils = set "${args_utils} -p${arg}" - end_if -end -args_utils = trim "${args_utils}" -set_env CARGO_MAKE_TASK_BUILD_UTILS_ARGS "${args_utils}" -'''] - -### tasks - -[tasks.default] -description = "## *DEFAULT* Build (debug-mode) and test project" -category = "[project]" -dependencies = ["action-build-debug", "test-terse"] - -## - -[tasks.build] -description = "## Build (release-mode) project" -category = "[project]" -dependencies = ["core::pre-build", "action-build-release", "core::post-build"] - -[tasks.build-debug] -description = "## Build (debug-mode) project" -category = "[project]" -dependencies = ["action-build-debug"] - -[tasks.build-examples] -description = "## Build (release-mode) project example(s); usage: `cargo make (build-examples | examples) [EXAMPLE]...`" -category = "[project]" -dependencies = ["core::pre-build", "action-build-examples", "core::post-build"] - -[tasks.build-features] -description = "## Build (with features; release-mode) project; usage: `cargo make (build-features | features) FEATURE...`" -category = "[project]" -dependencies = ["core::pre-build", "action-build-features", "core::post-build"] - -[tasks.build-release] -alias = "build" - -[tasks.debug] -alias = "build-debug" - -[tasks.example] -description = "hidden singular-form alias for 'examples'" -category = "[project]" -dependencies = ["examples"] - -[tasks.examples] -alias = "build-examples" - -[tasks.features] -alias = "build-features" - -[tasks.format] -description = "## Format code files (with `cargo fmt`; includes tests)" -category = "[project]" -dependencies = ["action-format", "action-format-tests"] - -[tasks.help] -description = "## Display help" -category = "[project]" -dependencies = ["action-display-help"] - -[tasks.install] -description = "## Install project binary (to $HOME/.cargo/bin)" -category = "[project]" -command = "cargo" -args = ["install", "--path", "."] - -[tasks.lint] -description = "## Display lint report" -category = "[project]" -dependencies = ["action-clippy", "action-fmt_report"] - -[tasks.release] -alias = "build" - -[tasks.test] -description = "## Run project tests" -category = "[project]" -dependencies = ["core::pre-test", "core::test", "core::post-test"] - -[tasks.test-terse] -description = "## Run project tests (with terse/summary output)" -category = "[project]" -dependencies = ["core::pre-test", "action-test_quiet", "core::post-test"] - -[tasks.test-util] -description = "## Test (individual) utilities; usage: `cargo make (test-util | test-uutil) [UTIL_NAME...]`" -category = "[project]" -dependencies = ["action-test-utils"] - -[tasks.test-utils] -description = "hidden plural-form alias for 'test-util'" -category = "[project]" -dependencies = ["test-util"] - -[tasks.test-uutil] -description = "hidden alias for 'test-util'" -category = "[project]" -dependencies = ["test-util"] - -[tasks.test-uutils] -description = "hidden alias for 'test-util'" -category = "[project]" -dependencies = ["test-util"] - -[tasks.uninstall] -description = "## Remove project binary (from $HOME/.cargo/bin)" -category = "[project]" -command = "cargo" -args = ["uninstall"] - -[tasks.util] -description = "## Build (individual; release-mode) utilities; usage: `cargo make (util | uutil) [UTIL_NAME...]`" -category = "[project]" -dependencies = [ - "core::pre-build", - "action-determine-utils", - "action-build-utils", - "core::post-build", -] - -[tasks.utils] -description = "hidden plural-form alias for 'util'" -category = "[project]" -dependencies = ["util"] - -[tasks.uutil] -description = "hidden alias for 'util'" -category = "[project]" -dependencies = ["util"] - -[tasks.uutils] -description = "hidden plural-form alias for 'util'" -category = "[project]" -dependencies = ["util"] - -### actions - -[tasks.action-build-release] -description = "`cargo build --release`" -command = "cargo" -args = ["build", "--release", "@@split(CARGO_MAKE_CARGO_BUILD_TEST_FLAGS, )"] - -[tasks.action-build-debug] -description = "`cargo build`" -command = "cargo" -args = ["build", "@@split(CARGO_MAKE_CARGO_BUILD_TEST_FLAGS, )"] - -[tasks.action-build-examples] -description = "`cargo build (--examples|(--example EXAMPLE)...)`" -command = "cargo" -args = [ - "build", - "--release", - "@@split(CARGO_MAKE_CARGO_BUILD_TEST_FLAGS, )", - "${CARGO_MAKE_TASK_BUILD_EXAMPLES_ARGS}", -] - -[tasks.action-build-features] -description = "`cargo build --release --features FEATURES`" -command = "cargo" -args = [ - "build", - "--release", - "--no-default-features", - "--features", - "${CARGO_MAKE_TASK_BUILD_FEATURES_ARGS}", -] - -[tasks.action-build-utils] -description = "Build individual utilities" -dependencies = ["action-determine-utils"] -command = "cargo" -# args = ["build", "@@remove-empty(CARGO_MAKE_TASK_BUILD_UTILS_ARGS)" ] -args = ["build", "--release", "@@split(CARGO_MAKE_TASK_BUILD_UTILS_ARGS, )"] - -[tasks.action-clippy] -description = "`cargo clippy` lint report" -command = "cargo" -args = ["clippy", "@@split(CARGO_MAKE_CARGO_BUILD_TEST_FLAGS, )"] - -[tasks.action-determine-utils] -script_runner = "@duckscript" -script = [''' -package_options = get_env CARGO_MAKE_TASK_BUILD_UTILS_ARGS -if is_empty "${package_options}" - show_utils = get_env CARGO_MAKE_VAR_SHOW_UTILS - features = get_env CARGO_MAKE_VAR_BUILD_TEST_FEATURES - if not is_empty "${features}" - result = exec "${show_utils}" --features "${features}" - else - result = exec "${show_utils}" - endif - set_env CARGO_MAKE_VAR_UTILS ${result.stdout} - utils = array %{result.stdout} - for util in ${utils} - if not is_empty "${util}" - if not starts_with "${util}" "uu_" - util = set "uu_${util}" - end_if - package_options = set "${package_options} -p${util}" - end_if - end - package_options = trim "${package_options}" -end_if -set_env CARGO_MAKE_TASK_BUILD_UTILS_ARGS "${package_options}" -'''] - -[tasks.action-determine-tests] -script_runner = "@duckscript" -script = [''' -test_files = glob_array tests/**/*.rs -for file in ${test_files} - file = replace "${file}" "\\" "/" - if not is_empty ${file} - if is_empty "${tests}" - tests = set "${file}" - else - tests = set "${tests} ${file}" - end_if - end_if -end -set_env CARGO_MAKE_VAR_TESTS "${tests}" -'''] - -[tasks.action-format] -description = "`cargo fmt`" -command = "cargo" -args = ["fmt"] - -[tasks.action-format-tests] -description = "`cargo fmt` tests" -dependencies = ["action-determine-tests"] -command = "cargo" -args = ["fmt", "--", "@@split(CARGO_MAKE_VAR_TESTS, )"] - -[tasks.action-fmt] -alias = "action-format" - -[tasks.action-fmt_report] -description = "`cargo fmt` lint report" -command = "cargo" -args = ["fmt", "--", "--check"] - -[tasks.action-spellcheck-codespell] -description = "`codespell` spellcheck repository" -command = "codespell" # (from `pip install codespell`) -args = [ - ".", - "--skip=*/.git,./target,./tests/fixtures", - "--ignore-words-list=mut,od", -] - -[tasks.action-test-utils] -description = "Build individual utilities" -dependencies = ["action-determine-utils"] -command = "cargo" -# args = ["build", "@@remove-empty(CARGO_MAKE_TASK_BUILD_UTILS_ARGS)" ] -args = ["test", "@@split(CARGO_MAKE_TASK_BUILD_UTILS_ARGS, )"] - -[tasks.action-test_quiet] -description = "Test (in `--quiet` mode)" -command = "cargo" -args = ["test", "--quiet", "@@split(CARGO_MAKE_CARGO_BUILD_TEST_FLAGS, )"] - -[tasks.action-display-help] -script_runner = "@duckscript" -script = [''' - echo "" - echo "usage: `cargo make TARGET [ARGS...]`" - echo "" - echo "TARGETs:" - echo "" - result = exec "cargo" make --list-all-steps - # set_env CARGO_MAKE_VAR_UTILS ${result.stdout} - # echo ${result.stdout} - lines = split ${result.stdout} "\n" - # echo ${lines} - for line in ${lines} - if not is_empty ${line} - if contains ${line} " - ##" - line_segments = split ${line} " - ##" - desc = array_pop ${line_segments} - desc = trim ${desc} - target = array_pop ${line_segments} - target = trim ${target} - l = length ${target} - r = range 0 18 - spacing = set "" - for i in ${r} - if greater_than ${i} ${l} - spacing = set "${spacing} " - end_if - end - echo ${target}${spacing}${desc} - end_if - end_if - end - echo "" -'''] From 13ea1fc1d163ea5756c6b0bd1ab24a1b9ccbdeff Mon Sep 17 00:00:00 2001 From: mattsu <35655889+mattsu2020@users.noreply.github.com> Date: Sat, 6 Dec 2025 03:50:34 +0900 Subject: [PATCH 161/182] chmod:fix safe traversal/access (#9554) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(chmod): use dirfd for recursive subdirectory traversal - Update chmod recursive logic to use directory file descriptors instead of full paths for subdirectories - Improves performance, avoids path length issues, and ensures dirfd-relative openat calls - Add test to verify strace output shows no AT_FDCWD with multi-component paths * test(chmod): add spell-check ignore for dirfd, subdirs, openat, FDCWD Added a spell-checker ignore directive in the chmod test file to suppress false positives for legitimate technical terms used in Unix API calls. * test(chmod): enforce strace requirement in recursive test, fail fast instead of skip Previously, the test_chmod_recursive_uses_dirfd_for_subdirs test skipped gracefully if strace was unavailable, without failing. This change enforces the strace dependency by failing the test immediately if strace is not installed or runnable, ensuring the test runs reliably in environments where it is expected to pass, and preventing silent skips. * ci: install strace in Ubuntu CI jobs for debugging system calls Add installation of strace tool on Ubuntu runners in both individual build/test and feature build/test jobs. This enables tracing system calls during execution, aiding in debugging and performance analysis within the CI/CD pipeline. Updated existing apt-get commands and added conditional steps for Linux-only installations. * ci: Add strace installation to Ubuntu-based CI workflows Install strace on ubuntu-latest runners across multiple jobs to enable system call tracing for testing purposes, ensuring compatibility with tests that require this debugging tool. This includes updating package lists in existing installation steps. * chore(build): install strace and prevent apt prompts in Cross.toml pre-build Modified the pre-build command to install strace utility for debugging and added -y flag to apt-get install to skip prompts, ensuring non-interactive builds. * feat(build): support Alpine-based cross images in pre-build Detect package manager (apt vs apk) to install tzdata and strace in both Debian/Ubuntu and Alpine *-musl targets. Added fallback warning for unsupported managers. This ensures strace is available for targets using Alpine, which doesn't have apt-get. * refactor(build): improve pre-build script readability by using multi-line strings Replace escaped multi-line string with triple-quoted string for better readability in Cross.toml. * feat(ci): install strace in WSL2 GitHub Actions workflow Install strace utility in the WSL2 environment to support tracing system calls during testing. Minor update to Cross.toml spell-checker ignore list for consistency with change. * ci(wsl2): install strace as root with non-interactive apt-get Updated the WSL2 workflow step to use root shell (wsl-bash-root) for installing strace, removing sudo calls and adding DEBIAN_FRONTEND=noninteractive to prevent prompts. This improves CI reliability by ensuring direct root access and automated, interrupt-free package installation. * ci: Move strace installation to user shell and update spell ignore Fix WSL2 GitHub Actions workflow by installing strace as the user instead of root for better permission handling, and add "noninteractive" to the spell-checker ignore comment for consistency with the new apt-get command. This ensures the tool is available in the testing environment without unnecessary privilege escalation. * chore: ci: remove unused strace installation from CI workflows Remove strace package installation from multiple GitHub Actions workflow files (CICD.yml, l10n.yml, wsl2.yml). Strace was historically installed in Ubuntu jobs for debugging system calls, but it's no longer required for the tests and builds, reducing CI setup time and dependencies. * ci: add strace installation and fix spell-checker comments in CI files - Install strace package in CICD workflow to support safe traversal verification for utilities like rm, chmod, chown, chgrp, mv, and du, enabling syscall tracing for testing. - Clean up spell-checker ignore comments in wsl2.yml and Cross.toml by removing misplaced flags.第二个测试产品**ci: add strace installation and fix spell-checker comments in CI files** - Install strace package in CICD workflow to support safe traversal verification for utilities like rm, chmod, chown, chgrp, mv, and du, enabling syscall tracing for testing. - Clean up spell-checker ignore comments in wsl2.yml and Cross.toml by removing misplaced flags. * test: add regression guard for recursive chmod dirfd-relative traversal Add a check in check-safe-traversal.sh to ensure recursive chmod operations use dirfd-relative openat calls instead of AT_FDCWD with multi-component paths, preventing potential race conditions. Ignore the corresponding Rust test as it is now covered by this shell script guard. --- src/uu/chmod/src/chmod.rs | 19 +++++++++++++-- tests/by-util/test_chmod.rs | 46 ++++++++++++++++++++++++++++++++++++ util/check-safe-traversal.sh | 5 ++++ 3 files changed, 68 insertions(+), 2 deletions(-) diff --git a/src/uu/chmod/src/chmod.rs b/src/uu/chmod/src/chmod.rs index c782ad429..15b608af6 100644 --- a/src/uu/chmod/src/chmod.rs +++ b/src/uu/chmod/src/chmod.rs @@ -522,9 +522,24 @@ impl Chmoder { .safe_chmod_file(&entry_path, dir_fd, &entry_name, meta.mode() & 0o7777) .and(r); - // Recurse into subdirectories + // Recurse into subdirectories using the existing directory fd if meta.is_dir() { - r = self.walk_dir_with_context(&entry_path, false).and(r); + match dir_fd.open_subdir(&entry_name) { + Ok(child_dir_fd) => { + r = self.safe_traverse_dir(&child_dir_fd, &entry_path).and(r); + } + Err(err) => { + let error = if err.kind() == std::io::ErrorKind::PermissionDenied { + ChmodError::PermissionDenied( + entry_path.to_string_lossy().to_string(), + ) + .into() + } else { + err.into() + }; + r = r.and(Err(error)); + } + } } } } diff --git a/tests/by-util/test_chmod.rs b/tests/by-util/test_chmod.rs index 1378aab00..e4d4b0284 100644 --- a/tests/by-util/test_chmod.rs +++ b/tests/by-util/test_chmod.rs @@ -2,6 +2,7 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +// spell-checker:ignore (words) dirfd subdirs openat FDCWD use std::fs::{OpenOptions, Permissions, metadata, set_permissions}; use std::os::unix::fs::{OpenOptionsExt, PermissionsExt}; @@ -1280,6 +1281,51 @@ fn test_chmod_non_utf8_paths() { ); } +#[cfg(all(target_os = "linux", feature = "chmod"))] +#[test] +#[ignore = "covered by util/check-safe-traversal.sh"] +fn test_chmod_recursive_uses_dirfd_for_subdirs() { + use std::process::Command; + use uutests::get_tests_binary; + + // strace is required; fail fast if it is missing or not runnable + let output = Command::new("strace") + .arg("-V") + .output() + .expect("strace not found; install strace to run this test"); + assert!( + output.status.success(), + "strace -V failed; ensure strace is installed and usable" + ); + + let (at, _ucmd) = at_and_ucmd!(); + at.mkdir("x"); + at.mkdir("x/y"); + at.mkdir("x/y/z"); + + let log_path = at.plus_as_string("strace.log"); + + let status = Command::new("strace") + .arg("-e") + .arg("openat") + .arg("-o") + .arg(&log_path) + .arg(get_tests_binary!()) + .args(["chmod", "-R", "+x", "x"]) + .current_dir(&at.subdir) + .status() + .expect("failed to run strace"); + assert!(status.success(), "strace run failed"); + + let log = at.read("strace.log"); + + // Regression guard: ensure recursion uses dirfd-relative openat instead of AT_FDCWD with a multi-component path + assert!( + !log.contains("openat(AT_FDCWD, \"x/y"), + "chmod recursed using AT_FDCWD with a multi-component path; expected dirfd-relative openat" + ); +} + #[test] fn test_chmod_colored_output() { // Test colored help message diff --git a/util/check-safe-traversal.sh b/util/check-safe-traversal.sh index ed3c5a78e..8dc9b04cf 100755 --- a/util/check-safe-traversal.sh +++ b/util/check-safe-traversal.sh @@ -173,6 +173,11 @@ fi if echo "$AVAILABLE_UTILS" | grep -q "chmod"; then cp -r test_dir test_chmod check_utility "chmod" "openat,fchmodat,newfstatat,chmod" "openat fchmodat" "-R 755 test_chmod" "recursive_chmod" + + # Additional regression guard: ensure recursion uses dirfd-relative openat, not AT_FDCWD with a multi-component path + if grep -q 'openat(AT_FDCWD, "test_chmod/' strace_chmod_recursive_chmod.log; then + fail_immediately "chmod recursed using AT_FDCWD with a multi-component path; expected dirfd-relative openat" + fi fi # Test chown - should use openat, fchownat, newfstatat From 667011573d431c907fea7cc9fff231ce1b0dac75 Mon Sep 17 00:00:00 2001 From: Chris Dryden Date: Fri, 5 Dec 2025 14:02:44 -0500 Subject: [PATCH 162/182] Merge pull request #9561 from ChrisDryden/seq_benches seq: adding large integers benchmarks --- src/uu/seq/benches/seq_bench.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/uu/seq/benches/seq_bench.rs b/src/uu/seq/benches/seq_bench.rs index d8c52131d..11956e8c0 100644 --- a/src/uu/seq/benches/seq_bench.rs +++ b/src/uu/seq/benches/seq_bench.rs @@ -15,6 +15,14 @@ fn seq_integers(bencher: Bencher) { }); } +/// Benchmark large integer +#[divan::bench] +fn seq_large_integers(bencher: Bencher) { + bencher.bench(|| { + black_box(run_util_function(uumain, &["4e10003", "4e10003"])); + }); +} + /// Benchmark sequence with custom separator #[divan::bench] fn seq_custom_separator(bencher: Bencher) { From 5b261bc1af5234ae4a469ababcbf9ca999ca47e5 Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Fri, 5 Dec 2025 23:07:16 +0100 Subject: [PATCH 163/182] du: handle `--files0-from=-` with piped in `-` (#8985) * du: handle --files0-from=- with piped in '-' * build-gnu.sh: remove incorrect string replacement in tests/du/files0-from.pl --------- Co-authored-by: Sylvestre Ledru --- src/uu/du/locales/en-US.ftl | 1 + src/uu/du/locales/fr-FR.ftl | 1 + src/uu/du/src/du.rs | 10 ++++++---- tests/by-util/test_du.rs | 16 +++++++++++++--- util/build-gnu.sh | 1 - 5 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/uu/du/locales/en-US.ftl b/src/uu/du/locales/en-US.ftl index b503d8d53..bd6c095ba 100644 --- a/src/uu/du/locales/en-US.ftl +++ b/src/uu/du/locales/en-US.ftl @@ -69,6 +69,7 @@ du-error-printing-thread-panicked = Printing thread panicked. du-error-invalid-suffix = invalid suffix in --{ $option } argument { $value } du-error-invalid-argument = invalid --{ $option } argument { $value } du-error-argument-too-large = --{ $option } argument { $value } too large +du-error-hyphen-file-name-not-allowed = when reading file names from standard input, no file name of '-' allowed # Verbose/status messages du-verbose-ignored = { $path } ignored diff --git a/src/uu/du/locales/fr-FR.ftl b/src/uu/du/locales/fr-FR.ftl index e89385213..81bc80c71 100644 --- a/src/uu/du/locales/fr-FR.ftl +++ b/src/uu/du/locales/fr-FR.ftl @@ -69,6 +69,7 @@ du-error-printing-thread-panicked = Le thread d'affichage a paniqué. du-error-invalid-suffix = suffixe invalide dans l'argument --{ $option } { $value } du-error-invalid-argument = argument --{ $option } invalide { $value } du-error-argument-too-large = argument --{ $option } { $value } trop grand +du-error-hyphen-file-name-not-allowed = le nom de fichier '-' n'est pas autorisé lors de la lecture de l'entrée standard # Messages verbeux/de statut du-verbose-ignored = { $path } ignoré diff --git a/src/uu/du/src/du.rs b/src/uu/du/src/du.rs index 522252a8b..f57228d76 100644 --- a/src/uu/du/src/du.rs +++ b/src/uu/du/src/du.rs @@ -2,16 +2,15 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +// // spell-checker:ignore fstatat openat dirfd use clap::{Arg, ArgAction, ArgMatches, Command, builder::PossibleValue}; use glob::Pattern; use std::collections::HashSet; use std::env; -use std::ffi::OsStr; -use std::ffi::OsString; -use std::fs::Metadata; -use std::fs::{self, DirEntry, File}; +use std::ffi::{OsStr, OsString}; +use std::fs::{self, DirEntry, File, Metadata}; use std::io::{BufRead, BufReader, stdout}; #[cfg(not(windows))] use std::os::unix::fs::MetadataExt; @@ -942,6 +941,9 @@ fn read_files_from(file_name: &OsStr) -> Result, std::io::Error> { translate!("du-error-invalid-zero-length-file-name", "file" => file_name.to_string_lossy(), "line" => line_number) ); set_exit_code(1); + } else if path == b"-" && file_name == "-" { + show_error!("{}", translate!("du-error-hyphen-file-name-not-allowed")); + set_exit_code(1); } else { let p = PathBuf::from(&*uucore::os_str_from_bytes(&path).unwrap()); if !paths.contains(&p) { diff --git a/tests/by-util/test_du.rs b/tests/by-util/test_du.rs index 224626b21..bc97cb28f 100644 --- a/tests/by-util/test_du.rs +++ b/tests/by-util/test_du.rs @@ -5,17 +5,16 @@ // spell-checker:ignore (paths) atim sublink subwords azerty azeaze xcwww azeaz amaz azea qzerty tazerty tsublink testfile1 testfile2 filelist fpath testdir testfile // spell-checker:ignore selfref ELOOP smallfile + #[cfg(not(windows))] use regex::Regex; -use uutests::at_and_ucmd; -use uutests::new_ucmd; #[cfg(not(target_os = "windows"))] use uutests::unwrap_or_return; use uutests::util::TestScenario; #[cfg(not(target_os = "windows"))] use uutests::util::expected_result; -use uutests::util_name; +use uutests::{at_and_ucmd, new_ucmd, util_name}; #[cfg(not(target_os = "openbsd"))] const SUB_DIR: &str = "subdir/deeper"; @@ -1399,6 +1398,17 @@ fn test_du_files0_from_stdin_with_invalid_zero_length_file_names() { .stderr_contains("-:2: invalid zero-length file name"); } +#[test] +fn test_du_files0_from_stdin_with_stdin_as_input() { + new_ucmd!() + .arg("--files0-from=-") + .pipe_in("-") + .fails_with_code(1) + .stderr_is( + "du: when reading file names from standard input, no file name of '-' allowed\n", + ); +} + #[test] fn test_du_files0_from_dir() { let ts = TestScenario::new(util_name!()); diff --git a/util/build-gnu.sh b/util/build-gnu.sh index 532e90592..223fc895b 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -285,7 +285,6 @@ test -f "${UU_BUILD_DIR}/getlimits" || cp src/getlimits "${UU_BUILD_DIR}" # Remove the extra output check "${SED}" -i -e "s|Try '\$prog --help' for more information.\\\n||" tests/du/files0-from.pl -"${SED}" -i -e "s|when reading file names from stdin, no file name of\"|-: No such file or directory\n\"|" -e "s| '-' allowed\\\n||" tests/du/files0-from.pl "${SED}" -i -e "s|-: No such file or directory|cannot access '-': No such file or directory|g" tests/du/files0-from.pl # Skip the move-dir-while-traversing test - our implementation uses safe traversal with openat() From 627a0bf2d067494a57de59cce0e68bdbd718ffb2 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 5 Dec 2025 22:57:23 +0100 Subject: [PATCH 164/182] tail: batch inotify events to prevent redundant headers after SIGSTOP/SIGCONT Hopefully will fix the intermittent tests/tail/overlay-headers --- src/uu/tail/src/follow/watch.rs | 34 ++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/src/uu/tail/src/follow/watch.rs b/src/uu/tail/src/follow/watch.rs index 9b0333efb..11e367918 100644 --- a/src/uu/tail/src/follow/watch.rs +++ b/src/uu/tail/src/follow/watch.rs @@ -548,13 +548,37 @@ pub fn follow(mut observer: Observer, settings: &Settings) -> UResult<()> { } let mut paths = vec![]; // Paths worth checking for new content to print + + // Helper closure to process a single event + let process_event = |observer: &mut Observer, + event: notify::Event, + settings: &Settings, + paths: &mut Vec| + -> UResult<()> { + if let Some(event_path) = event.paths.first() { + if observer.files.contains_key(event_path) { + // Handle Event if it is about a path that we are monitoring + let new_paths = observer.handle_event(&event, settings)?; + for p in new_paths { + if !paths.contains(&p) { + paths.push(p); + } + } + } + } + Ok(()) + }; + match rx_result { Ok(Ok(event)) => { - if let Some(event_path) = event.paths.first() { - if observer.files.contains_key(event_path) { - // Handle Event if it is about a path that we are monitoring - paths = observer.handle_event(&event, settings)?; - } + process_event(&mut observer, event, settings, &mut paths)?; + + // Drain any additional pending events to batch them together. + // This prevents redundant headers when multiple inotify events + // are queued (e.g., after resuming from SIGSTOP). + while let Ok(Ok(event)) = observer.watcher_rx.as_mut().unwrap().receiver.try_recv() + { + process_event(&mut observer, event, settings, &mut paths)?; } } Ok(Err(notify::Error { From 15b2df9d1edee55763e723d83d893101c9ede83f Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Wed, 19 Nov 2025 10:40:04 +0100 Subject: [PATCH 165/182] ptx: implement GNU mode with dumb terminal format --- src/uu/ptx/src/ptx.rs | 78 +++++++++++++++++++++++++++++++++++---- tests/by-util/test_ptx.rs | 8 ++++ 2 files changed, 78 insertions(+), 8 deletions(-) diff --git a/src/uu/ptx/src/ptx.rs b/src/uu/ptx/src/ptx.rs index e63d27599..d3b9d103c 100644 --- a/src/uu/ptx/src/ptx.rs +++ b/src/uu/ptx/src/ptx.rs @@ -197,9 +197,6 @@ struct WordRef { #[derive(Debug, Error)] enum PtxError { - #[error("{}", translate!("ptx-error-dumb-format"))] - DumbFormat, - #[error("{}", translate!("ptx-error-not-implemented", "feature" => (*.0)))] NotImplemented(&'static str), @@ -216,8 +213,6 @@ fn get_config(matches: &clap::ArgMatches) -> UResult { config.gnu_ext = false; config.format = OutFormat::Roff; "[^ \t\n]+".clone_into(&mut config.context_regex); - } else { - return Err(PtxError::NotImplemented("GNU extensions").into()); } if matches.contains_id(options::SENTENCE_REGEXP) { return Err(PtxError::NotImplemented("-S").into()); @@ -589,6 +584,69 @@ fn format_tex_line( output } +fn format_dumb_line( + config: &Config, + word_ref: &WordRef, + line: &str, + chars_line: &[char], + reference: &str, +) -> String { + let (tail, before, keyword, after, head) = + prepare_line_chunks(config, word_ref, line, chars_line, reference); + + // Calculate the position for the left part + // The left part consists of tail (if present) + space + before + let left_part = if tail.is_empty() { + before + } else if before.is_empty() { + tail + } else { + format!("{tail} {before}") + }; + + // Calculate the position for the right part + let right_part = if head.is_empty() { + after + } else if after.is_empty() { + head + } else { + format!("{after} {head}") + }; + + // Calculate the width for the left half (before the keyword) + let half_width = config.line_width / 2; + + // Right-justify the left part within the left half + let padding = if left_part.len() < half_width { + half_width - left_part.len() + } else { + 0 + }; + + // Build the output line with padding, left part, gap, keyword, and right part + let mut output = String::new(); + output.push_str(&" ".repeat(padding)); + output.push_str(&left_part); + + // Add gap before keyword + output.push_str(&" ".repeat(config.gap_size)); + + output.push_str(&keyword); + output.push_str(&right_part); + + // Add reference if needed + if config.auto_ref || config.input_ref { + if config.right_ref { + output.push(' '); + output.push_str(reference); + } else { + output = format!("{reference} {output}"); + } + } + + output +} + fn format_roff_field(s: &str) -> String { s.replace('\"', "\"\"") } @@ -716,9 +774,13 @@ fn write_traditional_output( &chars_lines[word_ref.local_line_nr], &reference, ), - OutFormat::Dumb => { - return Err(PtxError::DumbFormat.into()); - } + OutFormat::Dumb => format_dumb_line( + config, + word_ref, + &lines[word_ref.local_line_nr], + &chars_lines[word_ref.local_line_nr], + &reference, + ), }; writeln!(writer, "{output_line}") .map_err_context(|| translate!("ptx-error-write-failed"))?; diff --git a/tests/by-util/test_ptx.rs b/tests/by-util/test_ptx.rs index 3ff36a1c6..464dcf6ae 100644 --- a/tests/by-util/test_ptx.rs +++ b/tests/by-util/test_ptx.rs @@ -256,3 +256,11 @@ fn test_utf8() { .succeeds() .stdout_only("\\xx {}{it’s}{disabled}{}{}\n\\xx {}{}{it’s}{ disabled}{}\n"); } + +#[test] +fn test_gnu_mode_dumb_format() { + // Test GNU mode (dumb format) - the default mode without -G flag + new_ucmd!().pipe_in("a b").succeeds().stdout_only( + " a b\n a b\n", + ); +} From 11e77c72d43f53e4d3f5e2c0da4b3317fd7552d7 Mon Sep 17 00:00:00 2001 From: Martin Kunkel Date: Sat, 6 Dec 2025 10:16:56 +0000 Subject: [PATCH 166/182] uucore: mode parsing: support comma-separated mode Parsing in uucore::mode did not support multiple mode chunks separated by commas, e.g. "ug+rw,o+r" --- src/uucore/src/lib/features/mode.rs | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/uucore/src/lib/features/mode.rs b/src/uucore/src/lib/features/mode.rs index 50ed8c97c..323830d76 100644 --- a/src/uucore/src/lib/features/mode.rs +++ b/src/uucore/src/lib/features/mode.rs @@ -139,21 +139,16 @@ fn parse_change(mode: &str, fperm: u32, considering_dir: bool) -> (u32, usize) { #[allow(clippy::unnecessary_cast)] pub fn parse_mode(mode: &str) -> Result { - #[cfg(all( - not(target_os = "freebsd"), - not(target_vendor = "apple"), - not(target_os = "android") - ))] - let fperm = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH; - #[cfg(any(target_os = "freebsd", target_vendor = "apple", target_os = "android"))] - let fperm = (S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH) as u32; + let mut new_mode = (S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH) as u32; - let result = if mode.chars().any(|c| c.is_ascii_digit()) { - parse_numeric(fperm as u32, mode, true) - } else { - parse_symbolic(fperm as u32, mode, get_umask(), true) - }; - result.map(|mode| mode as mode_t) + for mode_chunk in mode.split(',') { + new_mode = if mode_chunk.chars().any(|c| c.is_ascii_digit()) { + parse_numeric(new_mode, mode_chunk, true)? + } else { + parse_symbolic(new_mode, mode_chunk, get_umask(), true)? + }; + } + Ok(new_mode as mode_t) } pub fn get_umask() -> u32 { @@ -202,4 +197,9 @@ mod test { assert_eq!(super::parse_mode("+100").unwrap(), 0o766); assert_eq!(super::parse_mode("-4").unwrap(), 0o662); } + + #[test] + fn multiple_modes() { + assert_eq!(super::parse_mode("+100,+010").unwrap(), 0o776); + } } From 38224017f90e9b31c1ccf7ef8b971ccfb20610e4 Mon Sep 17 00:00:00 2001 From: Etienne Cordonnier Date: Sat, 6 Dec 2025 14:19:39 +0100 Subject: [PATCH 167/182] timeout: remove FIXME in test This FIXME comment was added in 2021 ( 5431e947bc54242d6d6fb3b1b1c55f73dd1eade0 ). `timeout` is already in feat_require_unix_core, so having `true` and `false` on the machine running the test is quite reasonable and does not warrant a FIXME. Signed-off-by: Etienne Cordonnier --- tests/by-util/test_timeout.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/by-util/test_timeout.rs b/tests/by-util/test_timeout.rs index 27800f06d..ae0ce2e50 100644 --- a/tests/by-util/test_timeout.rs +++ b/tests/by-util/test_timeout.rs @@ -15,9 +15,6 @@ fn test_invalid_arg() { new_ucmd!().arg("--definitely-invalid").fails_with_code(125); } -// FIXME: this depends on the system having true and false in PATH -// the best solution is probably to generate some test binaries that we can call for any -// utility that requires executing another program (kill, for instance) #[test] fn test_subcommand_return_code() { new_ucmd!().arg("1").arg("true").succeeds(); From 8d590ca4cc1663024829f0dadcf7985a061b79df Mon Sep 17 00:00:00 2001 From: Etienne Cordonnier Date: Sat, 6 Dec 2025 14:37:41 +0100 Subject: [PATCH 168/182] timeout: cleanup return values (#9576) - remove "WaitingFailed" which is a duplicate of "CommandTimedOut" - replace hard-coded values 126 and 127 with enum values, remove TODO - fix misleading comment. we DO return CommandTimedOut even when preserve-status is not specified - add tests for exit values 126 and 127 Signed-off-by: Etienne Cordonnier --- src/uu/timeout/src/status.rs | 14 +++++++++----- src/uu/timeout/src/timeout.rs | 13 +++++-------- tests/by-util/test_timeout.rs | 15 +++++++++++++++ 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/src/uu/timeout/src/status.rs b/src/uu/timeout/src/status.rs index 422a13ea8..1134fb88d 100644 --- a/src/uu/timeout/src/status.rs +++ b/src/uu/timeout/src/status.rs @@ -19,18 +19,21 @@ use uucore::error::UError; /// assert_eq!(i32::from(ExitStatus::CommandTimedOut), 124); /// ``` pub(crate) enum ExitStatus { - /// When the child process times out and `--preserve-status` is not specified. + /// When the child process times out. CommandTimedOut, /// When `timeout` itself fails. TimeoutFailed, + /// When command is found but cannot be invoked (permission denied, etc.). + CannotInvoke, + + /// When command cannot be found. + CommandNotFound, + /// When a signal is sent to the child process or `timeout` itself. SignalSent(usize), - /// When there is a failure while waiting for the child process to terminate. - WaitingFailed, - /// When `SIGTERM` signal received. Terminated, } @@ -40,8 +43,9 @@ impl From for i32 { match exit_status { ExitStatus::CommandTimedOut => 124, ExitStatus::TimeoutFailed => 125, + ExitStatus::CannotInvoke => 126, + ExitStatus::CommandNotFound => 127, ExitStatus::SignalSent(s) => 128 + s as Self, - ExitStatus::WaitingFailed => 124, ExitStatus::Terminated => 143, } } diff --git a/src/uu/timeout/src/timeout.rs b/src/uu/timeout/src/timeout.rs index 94d469c7e..3e1a35c45 100644 --- a/src/uu/timeout/src/timeout.rs +++ b/src/uu/timeout/src/timeout.rs @@ -275,7 +275,7 @@ fn wait_or_kill_process( process.wait()?; Ok(ExitStatus::SignalSent(signal).into()) } - Err(_) => Ok(ExitStatus::WaitingFailed.into()), + Err(_) => Ok(ExitStatus::CommandTimedOut.into()), } } @@ -305,7 +305,6 @@ fn preserve_signal_info(signal: libc::c_int) -> libc::c_int { signal } -/// TODO: Improve exit codes, and make them consistent with the GNU Coreutils exit codes. fn timeout( cmd: &[String], duration: Duration, @@ -328,12 +327,10 @@ fn timeout( .stderr(Stdio::inherit()) .spawn() .map_err(|err| { - let status_code = if err.kind() == ErrorKind::NotFound { - // FIXME: not sure which to use - 127 - } else { - // FIXME: this may not be 100% correct... - 126 + let status_code = match err.kind() { + ErrorKind::NotFound => ExitStatus::CommandNotFound.into(), + ErrorKind::PermissionDenied => ExitStatus::CannotInvoke.into(), + _ => ExitStatus::CannotInvoke.into(), }; USimpleError::new( status_code, diff --git a/tests/by-util/test_timeout.rs b/tests/by-util/test_timeout.rs index 27800f06d..3db18679e 100644 --- a/tests/by-util/test_timeout.rs +++ b/tests/by-util/test_timeout.rs @@ -223,3 +223,18 @@ fn test_terminate_child_on_receiving_terminate() { .code_is(143) .stdout_contains("child received TERM"); } + +#[test] +fn test_command_not_found() { + // Test exit code 127 when command doesn't exist + new_ucmd!() + .args(&["1", "/this/command/definitely/does/not/exist"]) + .fails_with_code(127); +} + +#[test] +fn test_command_cannot_invoke() { + // Test exit code 126 when command exists but cannot be invoked + // Try to execute a directory (should give permission denied or similar) + new_ucmd!().args(&["1", "/"]).fails_with_code(126); +} From 4e653b5ec0d0e9f91a3a8f62c7e5050b94091edc Mon Sep 17 00:00:00 2001 From: Martin Kunkel Date: Sat, 6 Dec 2025 13:58:37 +0000 Subject: [PATCH 169/182] move mode parsing and tests from install to uucore --- src/uu/install/src/install.rs | 2 +- src/uu/install/src/mode.rs | 149 ---------------------------- src/uucore/src/lib/features/mode.rs | 144 ++++++++++++++++++++++++++- 3 files changed, 142 insertions(+), 153 deletions(-) diff --git a/src/uu/install/src/install.rs b/src/uu/install/src/install.rs index ab05c7ca0..582eb91ac 100644 --- a/src/uu/install/src/install.rs +++ b/src/uu/install/src/install.rs @@ -338,7 +338,7 @@ fn behavior(matches: &ArgMatches) -> UResult { let specified_mode: Option = if matches.contains_id(OPT_MODE) { let x = matches.get_one::(OPT_MODE).ok_or(1)?; - Some(mode::parse(x, considering_dir, 0).map_err(|err| { + Some(uucore::mode::parse(x, considering_dir, 0).map_err(|err| { show_error!( "{}", translate!("install-error-invalid-mode", "error" => err) diff --git a/src/uu/install/src/mode.rs b/src/uu/install/src/mode.rs index 5c29aaf77..96aae38c4 100644 --- a/src/uu/install/src/mode.rs +++ b/src/uu/install/src/mode.rs @@ -4,32 +4,8 @@ // file that was distributed with this source code. use std::fs; use std::path::Path; -#[cfg(not(windows))] -use uucore::mode; use uucore::translate; -/// Takes a user-supplied string and tries to parse to u16 mode bitmask. -/// Supports comma-separated mode strings like "ug+rwX,o+rX" (same as chmod). -pub fn parse(mode_string: &str, considering_dir: bool, umask: u32) -> Result { - // Split by commas and process each mode part sequentially - let mut current_mode: u32 = 0; - - for mode_part in mode_string.split(',') { - let mode_part = mode_part.trim(); - if mode_part.is_empty() { - continue; - } - - current_mode = if mode_part.chars().any(|c| c.is_ascii_digit()) { - mode::parse_numeric(current_mode, mode_part, considering_dir)? - } else { - mode::parse_symbolic(current_mode, mode_part, umask, considering_dir)? - }; - } - - Ok(current_mode) -} - /// chmod a file or directory on UNIX. /// /// Adapted from mkdir.rs. Handles own error printing. @@ -55,128 +31,3 @@ pub fn chmod(path: &Path, mode: u32) -> Result<(), ()> { // chmod on Windows only sets the readonly flag, which isn't even honored on directories Ok(()) } - -#[cfg(test)] -#[cfg(not(windows))] -mod tests { - use super::parse; - - #[test] - fn test_parse_numeric_mode() { - // Simple numeric mode - assert_eq!(parse("644", false, 0).unwrap(), 0o644); - assert_eq!(parse("755", false, 0).unwrap(), 0o755); - assert_eq!(parse("777", false, 0).unwrap(), 0o777); - assert_eq!(parse("600", false, 0).unwrap(), 0o600); - } - - #[test] - fn test_parse_numeric_mode_with_operator() { - // Numeric mode with + operator - assert_eq!(parse("+100", false, 0).unwrap(), 0o100); - assert_eq!(parse("+644", false, 0).unwrap(), 0o644); - - // Numeric mode with - operator (starting from 0, so nothing to remove) - assert_eq!(parse("-4", false, 0).unwrap(), 0); - // But if we first set a mode, then remove bits - assert_eq!(parse("644,-4", false, 0).unwrap(), 0o640); - } - - #[test] - fn test_parse_symbolic_mode() { - // Simple symbolic modes - assert_eq!(parse("u+x", false, 0).unwrap(), 0o100); - assert_eq!(parse("g+w", false, 0).unwrap(), 0o020); - assert_eq!(parse("o+r", false, 0).unwrap(), 0o004); - assert_eq!(parse("a+x", false, 0).unwrap(), 0o111); - } - - #[test] - fn test_parse_symbolic_mode_multiple_permissions() { - // Multiple permissions in one mode - assert_eq!(parse("u+rw", false, 0).unwrap(), 0o600); - assert_eq!(parse("ug+rwx", false, 0).unwrap(), 0o770); - assert_eq!(parse("a+rwx", false, 0).unwrap(), 0o777); - } - - #[test] - fn test_parse_comma_separated_modes() { - // Comma-separated mode strings (as mentioned in the doc comment) - assert_eq!(parse("ug+rwX,o+rX", false, 0).unwrap(), 0o664); - assert_eq!(parse("u+rwx,g+rx,o+r", false, 0).unwrap(), 0o754); - assert_eq!(parse("u+w,g+w,o+w", false, 0).unwrap(), 0o222); - } - - #[test] - fn test_parse_comma_separated_with_spaces() { - // Comma-separated with spaces (should be trimmed) - assert_eq!(parse("u+rw, g+rw, o+r", false, 0).unwrap(), 0o664); - assert_eq!(parse(" u+x , g+x ", false, 0).unwrap(), 0o110); - } - - #[test] - fn test_parse_mixed_numeric_and_symbolic() { - // Mix of numeric and symbolic modes - assert_eq!(parse("644,u+x", false, 0).unwrap(), 0o744); - assert_eq!(parse("u+rw,755", false, 0).unwrap(), 0o755); - } - - #[test] - fn test_parse_empty_string() { - // Empty string should return 0 - assert_eq!(parse("", false, 0).unwrap(), 0); - assert_eq!(parse(" ", false, 0).unwrap(), 0); - assert_eq!(parse(",,", false, 0).unwrap(), 0); - } - - #[test] - fn test_parse_with_umask() { - // Test with umask (affects symbolic modes when no level is specified) - let umask = 0o022; - assert_eq!(parse("+w", false, umask).unwrap(), 0o200); - // The umask should be respected for symbolic modes without explicit level - } - - #[test] - fn test_parse_considering_dir() { - // Test directory vs file mode differences - // For directories, X (capital X) should add execute permission - assert_eq!(parse("a+X", true, 0).unwrap(), 0o111); - // For files without execute, X should not add execute - assert_eq!(parse("a+X", false, 0).unwrap(), 0o000); - - // Numeric modes for directories preserve setuid/setgid bits - assert_eq!(parse("755", true, 0).unwrap(), 0o755); - } - - #[test] - fn test_parse_invalid_modes() { - // Invalid numeric mode (too large) - assert!(parse("10000", false, 0).is_err()); - - // Invalid operator - assert!(parse("u*rw", false, 0).is_err()); - - // Invalid symbolic mode - assert!(parse("invalid", false, 0).is_err()); - } - - #[test] - fn test_parse_complex_combinations() { - // Complex real-world examples - assert_eq!(parse("u=rwx,g=rx,o=r", false, 0).unwrap(), 0o754); - // To test removal, we need to first set permissions, then remove them - assert_eq!(parse("644,a-w", false, 0).unwrap(), 0o444); - assert_eq!(parse("644,g-r", false, 0).unwrap(), 0o604); - } - - #[test] - fn test_parse_sequential_application() { - // Test that comma-separated modes are applied sequentially - // First set to 644, then add execute for user - assert_eq!(parse("644,u+x", false, 0).unwrap(), 0o744); - - // First add user write, then set to 755 (should override) - assert_eq!(parse("u+w,755", false, 0).unwrap(), 0o755); - } -} diff --git a/src/uucore/src/lib/features/mode.rs b/src/uucore/src/lib/features/mode.rs index 323830d76..af2494737 100644 --- a/src/uucore/src/lib/features/mode.rs +++ b/src/uucore/src/lib/features/mode.rs @@ -137,6 +137,28 @@ fn parse_change(mode: &str, fperm: u32, considering_dir: bool) -> (u32, usize) { (srwx, pos) } +/// Takes a user-supplied string and tries to parse to u16 mode bitmask. +/// Supports comma-separated mode strings like "ug+rwX,o+rX" (same as chmod). +pub fn parse(mode_string: &str, considering_dir: bool, umask: u32) -> Result { + // Split by commas and process each mode part sequentially + let mut current_mode: u32 = 0; + + for mode_part in mode_string.split(',') { + let mode_part = mode_part.trim(); + if mode_part.is_empty() { + continue; + } + + current_mode = if mode_part.chars().any(|c| c.is_ascii_digit()) { + parse_numeric(current_mode, mode_part, considering_dir)? + } else { + parse_symbolic(current_mode, mode_part, umask, considering_dir)? + }; + } + + Ok(current_mode) +} + #[allow(clippy::unnecessary_cast)] pub fn parse_mode(mode: &str) -> Result { let mut new_mode = (S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH) as u32; @@ -178,7 +200,9 @@ pub fn get_umask() -> u32 { } #[cfg(test)] -mod test { +mod tests { + + use super::parse; #[test] fn symbolic_modes() { @@ -199,7 +223,121 @@ mod test { } #[test] - fn multiple_modes() { - assert_eq!(super::parse_mode("+100,+010").unwrap(), 0o776); + fn test_parse_numeric_mode() { + // Simple numeric mode + assert_eq!(parse("644", false, 0).unwrap(), 0o644); + assert_eq!(parse("755", false, 0).unwrap(), 0o755); + assert_eq!(parse("777", false, 0).unwrap(), 0o777); + assert_eq!(parse("600", false, 0).unwrap(), 0o600); + } + + #[test] + fn test_parse_numeric_mode_with_operator() { + // Numeric mode with + operator + assert_eq!(parse("+100", false, 0).unwrap(), 0o100); + assert_eq!(parse("+644", false, 0).unwrap(), 0o644); + + // Numeric mode with - operator (starting from 0, so nothing to remove) + assert_eq!(parse("-4", false, 0).unwrap(), 0); + // But if we first set a mode, then remove bits + assert_eq!(parse("644,-4", false, 0).unwrap(), 0o640); + } + + #[test] + fn test_parse_symbolic_mode() { + // Simple symbolic modes + assert_eq!(parse("u+x", false, 0).unwrap(), 0o100); + assert_eq!(parse("g+w", false, 0).unwrap(), 0o020); + assert_eq!(parse("o+r", false, 0).unwrap(), 0o004); + assert_eq!(parse("a+x", false, 0).unwrap(), 0o111); + } + + #[test] + fn test_parse_symbolic_mode_multiple_permissions() { + // Multiple permissions in one mode + assert_eq!(parse("u+rw", false, 0).unwrap(), 0o600); + assert_eq!(parse("ug+rwx", false, 0).unwrap(), 0o770); + assert_eq!(parse("a+rwx", false, 0).unwrap(), 0o777); + } + + #[test] + fn test_parse_comma_separated_modes() { + // Comma-separated mode strings (as mentioned in the doc comment) + assert_eq!(parse("ug+rwX,o+rX", false, 0).unwrap(), 0o664); + assert_eq!(parse("u+rwx,g+rx,o+r", false, 0).unwrap(), 0o754); + assert_eq!(parse("u+w,g+w,o+w", false, 0).unwrap(), 0o222); + } + + #[test] + fn test_parse_comma_separated_with_spaces() { + // Comma-separated with spaces (should be trimmed) + assert_eq!(parse("u+rw, g+rw, o+r", false, 0).unwrap(), 0o664); + assert_eq!(parse(" u+x , g+x ", false, 0).unwrap(), 0o110); + } + + #[test] + fn test_parse_mixed_numeric_and_symbolic() { + // Mix of numeric and symbolic modes + assert_eq!(parse("644,u+x", false, 0).unwrap(), 0o744); + assert_eq!(parse("u+rw,755", false, 0).unwrap(), 0o755); + } + + #[test] + fn test_parse_empty_string() { + // Empty string should return 0 + assert_eq!(parse("", false, 0).unwrap(), 0); + assert_eq!(parse(" ", false, 0).unwrap(), 0); + assert_eq!(parse(",,", false, 0).unwrap(), 0); + } + + #[test] + fn test_parse_with_umask() { + // Test with umask (affects symbolic modes when no level is specified) + let umask = 0o022; + assert_eq!(parse("+w", false, umask).unwrap(), 0o200); + // The umask should be respected for symbolic modes without explicit level + } + + #[test] + fn test_parse_considering_dir() { + // Test directory vs file mode differences + // For directories, X (capital X) should add execute permission + assert_eq!(parse("a+X", true, 0).unwrap(), 0o111); + // For files without execute, X should not add execute + assert_eq!(parse("a+X", false, 0).unwrap(), 0o000); + + // Numeric modes for directories preserve setuid/setgid bits + assert_eq!(parse("755", true, 0).unwrap(), 0o755); + } + + #[test] + fn test_parse_invalid_modes() { + // Invalid numeric mode (too large) + assert!(parse("10000", false, 0).is_err()); + + // Invalid operator + assert!(parse("u*rw", false, 0).is_err()); + + // Invalid symbolic mode + assert!(parse("invalid", false, 0).is_err()); + } + + #[test] + fn test_parse_complex_combinations() { + // Complex real-world examples + assert_eq!(parse("u=rwx,g=rx,o=r", false, 0).unwrap(), 0o754); + // To test removal, we need to first set permissions, then remove them + assert_eq!(parse("644,a-w", false, 0).unwrap(), 0o444); + assert_eq!(parse("644,g-r", false, 0).unwrap(), 0o604); + } + + #[test] + fn test_parse_sequential_application() { + // Test that comma-separated modes are applied sequentially + // First set to 644, then add execute for user + assert_eq!(parse("644,u+x", false, 0).unwrap(), 0o744); + + // First add user write, then set to 755 (should override) + assert_eq!(parse("u+w,755", false, 0).unwrap(), 0o755); } } From 22344cea9987949d32e59b8a3b19beb902de313d Mon Sep 17 00:00:00 2001 From: Martin Kunkel Date: Sat, 6 Dec 2025 14:16:25 +0000 Subject: [PATCH 170/182] Use new method from parse_mode as well --- src/uucore/src/lib/features/mode.rs | 70 ++++++++++++++++++----------- 1 file changed, 45 insertions(+), 25 deletions(-) diff --git a/src/uucore/src/lib/features/mode.rs b/src/uucore/src/lib/features/mode.rs index af2494737..a9477fa5f 100644 --- a/src/uucore/src/lib/features/mode.rs +++ b/src/uucore/src/lib/features/mode.rs @@ -137,39 +137,42 @@ fn parse_change(mode: &str, fperm: u32, considering_dir: bool) -> (u32, usize) { (srwx, pos) } -/// Takes a user-supplied string and tries to parse to u16 mode bitmask. +/// Modify a file mode based on a user-supplied string. /// Supports comma-separated mode strings like "ug+rwX,o+rX" (same as chmod). -pub fn parse(mode_string: &str, considering_dir: bool, umask: u32) -> Result { - // Split by commas and process each mode part sequentially - let mut current_mode: u32 = 0; +pub fn parse_chmod( + current_mode: u32, + mode_string: &str, + considering_dir: bool, + umask: u32, +) -> Result { + let mut new_mode: u32 = current_mode; + // Split by commas and process each mode part sequentially for mode_part in mode_string.split(',') { let mode_part = mode_part.trim(); if mode_part.is_empty() { continue; } - current_mode = if mode_part.chars().any(|c| c.is_ascii_digit()) { - parse_numeric(current_mode, mode_part, considering_dir)? + new_mode = if mode_part.chars().any(|c| c.is_ascii_digit()) { + parse_numeric(new_mode, mode_part, considering_dir)? } else { - parse_symbolic(current_mode, mode_part, umask, considering_dir)? + parse_symbolic(new_mode, mode_part, umask, considering_dir)? }; } - Ok(current_mode) + Ok(new_mode) +} + +/// Takes a user-supplied string and tries to parse to u32 mode bitmask. +pub fn parse(mode_string: &str, considering_dir: bool, umask: u32) -> Result { + parse_chmod(0, mode_string, considering_dir, umask) } #[allow(clippy::unnecessary_cast)] pub fn parse_mode(mode: &str) -> Result { let mut new_mode = (S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH) as u32; - - for mode_chunk in mode.split(',') { - new_mode = if mode_chunk.chars().any(|c| c.is_ascii_digit()) { - parse_numeric(new_mode, mode_chunk, true)? - } else { - parse_symbolic(new_mode, mode_chunk, get_umask(), true)? - }; - } + new_mode = parse_chmod(new_mode, mode, true, get_umask())?; Ok(new_mode as mode_t) } @@ -203,23 +206,25 @@ pub fn get_umask() -> u32 { mod tests { use super::parse; + use super::parse_chmod; + use super::parse_mode; #[test] - fn symbolic_modes() { - assert_eq!(super::parse_mode("u+x").unwrap(), 0o766); + fn test_symbolic_modes() { + assert_eq!(parse_mode("u+x").unwrap(), 0o766); assert_eq!( - super::parse_mode("+x").unwrap(), + parse_mode("+x").unwrap(), if crate::os::is_wsl_1() { 0o776 } else { 0o777 } ); - assert_eq!(super::parse_mode("a-w").unwrap(), 0o444); - assert_eq!(super::parse_mode("g-r").unwrap(), 0o626); + assert_eq!(parse_mode("a-w").unwrap(), 0o444); + assert_eq!(parse_mode("g-r").unwrap(), 0o626); } #[test] - fn numeric_modes() { - assert_eq!(super::parse_mode("644").unwrap(), 0o644); - assert_eq!(super::parse_mode("+100").unwrap(), 0o766); - assert_eq!(super::parse_mode("-4").unwrap(), 0o662); + fn test_numeric_modes() { + assert_eq!(parse_mode("644").unwrap(), 0o644); + assert_eq!(parse_mode("+100").unwrap(), 0o766); + assert_eq!(parse_mode("-4").unwrap(), 0o662); } #[test] @@ -340,4 +345,19 @@ mod tests { // First add user write, then set to 755 (should override) assert_eq!(parse("u+w,755", false, 0).unwrap(), 0o755); } + + #[test] + fn test_chmod_symbolic_modes() { + assert_eq!(parse_chmod(0o666, "u+x", false, 0).unwrap(), 0o766); + assert_eq!(parse_chmod(0o666, "+x", false, 0).unwrap(), 0o777); + assert_eq!(parse_chmod(0o666, "a-w", false, 0).unwrap(), 0o444); + assert_eq!(parse_chmod(0o666, "g-r", false, 0).unwrap(), 0o626); + } + + #[test] + fn test_chmod_numeric_modes() { + assert_eq!(parse_chmod(0o666, "644", false, 0).unwrap(), 0o644); + assert_eq!(parse_chmod(0o666, "+100", false, 0).unwrap(), 0o766); + assert_eq!(parse_chmod(0o666, "-4", false, 0).unwrap(), 0o662); + } } From 114be93e01158252092275a429df2ce413d8283b Mon Sep 17 00:00:00 2001 From: Martin Kunkel Date: Sat, 6 Dec 2025 14:27:03 +0000 Subject: [PATCH 171/182] Use common mode parsing in mkdirm, mkfifo, mknod --- src/uu/mkdir/src/mkdir.rs | 13 ++------ src/uu/mkfifo/src/mkfifo.rs | 14 ++------- src/uu/mknod/src/mknod.rs | 6 ++-- src/uucore/src/lib/features/mode.rs | 46 +++++++---------------------- 4 files changed, 19 insertions(+), 60 deletions(-) diff --git a/src/uu/mkdir/src/mkdir.rs b/src/uu/mkdir/src/mkdir.rs index a16be0c26..6ee610013 100644 --- a/src/uu/mkdir/src/mkdir.rs +++ b/src/uu/mkdir/src/mkdir.rs @@ -57,20 +57,11 @@ fn get_mode(_matches: &ArgMatches) -> Result { #[cfg(not(windows))] fn get_mode(matches: &ArgMatches) -> Result { // Not tested on Windows - let mut new_mode = DEFAULT_PERM; - if let Some(m) = matches.get_one::(options::MODE) { - for mode in m.split(',') { - if mode.chars().any(|c| c.is_ascii_digit()) { - new_mode = mode::parse_numeric(new_mode, m, true)?; - } else { - new_mode = mode::parse_symbolic(new_mode, mode, mode::get_umask(), true)?; - } - } - Ok(new_mode) + mode::parse_chmod(DEFAULT_PERM, m, true, mode::get_umask()) } else { // If no mode argument is specified return the mode derived from umask - Ok(!mode::get_umask() & 0o0777) + Ok(!mode::get_umask() & DEFAULT_PERM) } } diff --git a/src/uu/mkfifo/src/mkfifo.rs b/src/uu/mkfifo/src/mkfifo.rs index 572ea00b8..c55593dcb 100644 --- a/src/uu/mkfifo/src/mkfifo.rs +++ b/src/uu/mkfifo/src/mkfifo.rs @@ -119,19 +119,11 @@ pub fn uu_app() -> Command { fn calculate_mode(mode_option: Option<&String>) -> Result { let umask = uucore::mode::get_umask(); - let mut mode = 0o666; // Default mode for FIFOs + let mode = 0o666; // Default mode for FIFOs if let Some(m) = mode_option { - if m.chars().any(|c| c.is_ascii_digit()) { - mode = uucore::mode::parse_numeric(mode, m, false)?; - } else { - for item in m.split(',') { - mode = uucore::mode::parse_symbolic(mode, item, umask, false)?; - } - } + uucore::mode::parse_chmod(mode, m, false, umask) } else { - mode &= !umask; // Apply umask if no mode is specified + Ok(mode & !umask) // Apply umask if no mode is specified } - - Ok(mode) } diff --git a/src/uu/mknod/src/mknod.rs b/src/uu/mknod/src/mknod.rs index ca2640b68..cc22aee5f 100644 --- a/src/uu/mknod/src/mknod.rs +++ b/src/uu/mknod/src/mknod.rs @@ -225,8 +225,10 @@ pub fn uu_app() -> Command { ) } +#[allow(clippy::unnecessary_cast)] fn parse_mode(str_mode: &str) -> Result { - uucore::mode::parse_mode(str_mode) + let default_mode = (S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH) as u32; + uucore::mode::parse_chmod(default_mode, str_mode, true, uucore::mode::get_umask()) .map_err(|e| { translate!( "mknod-error-invalid-mode", @@ -237,7 +239,7 @@ fn parse_mode(str_mode: &str) -> Result { if mode > 0o777 { Err(translate!("mknod-error-mode-permission-bits-only")) } else { - Ok(mode) + Ok(mode as mode_t) } }) } diff --git a/src/uucore/src/lib/features/mode.rs b/src/uucore/src/lib/features/mode.rs index a9477fa5f..d562f1fe0 100644 --- a/src/uucore/src/lib/features/mode.rs +++ b/src/uucore/src/lib/features/mode.rs @@ -7,7 +7,7 @@ // spell-checker:ignore (vars) fperm srwx -use libc::{S_IRGRP, S_IROTH, S_IRUSR, S_IWGRP, S_IWOTH, S_IWUSR, mode_t, umask}; +use libc::umask; pub fn parse_numeric(fperm: u32, mut mode: &str, considering_dir: bool) -> Result { let (op, pos) = parse_op(mode).map_or_else(|_| (None, 0), |(op, pos)| (Some(op), pos)); @@ -169,13 +169,6 @@ pub fn parse(mode_string: &str, considering_dir: bool, umask: u32) -> Result Result { - let mut new_mode = (S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH) as u32; - new_mode = parse_chmod(new_mode, mode, true, get_umask())?; - Ok(new_mode as mode_t) -} - pub fn get_umask() -> u32 { // There's no portable way to read the umask without changing it. // We have to replace it and then quickly set it back, hopefully before @@ -207,24 +200,20 @@ mod tests { use super::parse; use super::parse_chmod; - use super::parse_mode; #[test] - fn test_symbolic_modes() { - assert_eq!(parse_mode("u+x").unwrap(), 0o766); - assert_eq!( - parse_mode("+x").unwrap(), - if crate::os::is_wsl_1() { 0o776 } else { 0o777 } - ); - assert_eq!(parse_mode("a-w").unwrap(), 0o444); - assert_eq!(parse_mode("g-r").unwrap(), 0o626); + fn test_chmod_symbolic_modes() { + assert_eq!(parse_chmod(0o666, "u+x", false, 0).unwrap(), 0o766); + assert_eq!(parse_chmod(0o666, "+x", false, 0).unwrap(), 0o777); + assert_eq!(parse_chmod(0o666, "a-w", false, 0).unwrap(), 0o444); + assert_eq!(parse_chmod(0o666, "g-r", false, 0).unwrap(), 0o626); } #[test] - fn test_numeric_modes() { - assert_eq!(parse_mode("644").unwrap(), 0o644); - assert_eq!(parse_mode("+100").unwrap(), 0o766); - assert_eq!(parse_mode("-4").unwrap(), 0o662); + fn test_chmod_numeric_modes() { + assert_eq!(parse_chmod(0o666, "644", false, 0).unwrap(), 0o644); + assert_eq!(parse_chmod(0o666, "+100", false, 0).unwrap(), 0o766); + assert_eq!(parse_chmod(0o666, "-4", false, 0).unwrap(), 0o662); } #[test] @@ -345,19 +334,4 @@ mod tests { // First add user write, then set to 755 (should override) assert_eq!(parse("u+w,755", false, 0).unwrap(), 0o755); } - - #[test] - fn test_chmod_symbolic_modes() { - assert_eq!(parse_chmod(0o666, "u+x", false, 0).unwrap(), 0o766); - assert_eq!(parse_chmod(0o666, "+x", false, 0).unwrap(), 0o777); - assert_eq!(parse_chmod(0o666, "a-w", false, 0).unwrap(), 0o444); - assert_eq!(parse_chmod(0o666, "g-r", false, 0).unwrap(), 0o626); - } - - #[test] - fn test_chmod_numeric_modes() { - assert_eq!(parse_chmod(0o666, "644", false, 0).unwrap(), 0o644); - assert_eq!(parse_chmod(0o666, "+100", false, 0).unwrap(), 0o766); - assert_eq!(parse_chmod(0o666, "-4", false, 0).unwrap(), 0o662); - } } From 63dbffa7f3d0c1134da535dec6117ae92d9c9784 Mon Sep 17 00:00:00 2001 From: Martin Kunkel Date: Sat, 6 Dec 2025 15:08:13 +0000 Subject: [PATCH 172/182] Add tests for multiple mode specifications --- tests/by-util/test_mkfifo.rs | 2 ++ tests/by-util/test_mknod.rs | 16 ++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/tests/by-util/test_mkfifo.rs b/tests/by-util/test_mkfifo.rs index 707adf71c..b90fc0c95 100644 --- a/tests/by-util/test_mkfifo.rs +++ b/tests/by-util/test_mkfifo.rs @@ -99,6 +99,8 @@ fn test_create_fifo_with_mode_and_umask() { test_fifo_creation("u-r,g-w,o+x", 0o022, "p-w-r--rwx"); // spell-checker:disable-line test_fifo_creation("a=rwx,o-w", 0o022, "prwxrwxr-x"); // spell-checker:disable-line test_fifo_creation("=rwx,o-w", 0o022, "prwxr-xr-x"); // spell-checker:disable-line + test_fifo_creation("ug+rw,o+r", 0o022, "prw-rw-rw-"); // spell-checker:disable-line + test_fifo_creation("u=rwx,g=rx,o=", 0o022, "prwxr-x---"); // spell-checker:disable-line } #[test] diff --git a/tests/by-util/test_mknod.rs b/tests/by-util/test_mknod.rs index 34136b828..5d2b08aec 100644 --- a/tests/by-util/test_mknod.rs +++ b/tests/by-util/test_mknod.rs @@ -154,6 +154,22 @@ fn test_mknod_mode_permissions() { } } +#[test] +fn test_mknod_mode_comma_separated() { + let ts = TestScenario::new(util_name!()); + ts.ucmd() + .arg("-m") + .arg("u=rwx,g=rx,o=") + .arg("test_file") + .arg("p") + .succeeds(); + assert!(ts.fixtures.is_fifo("test_file")); + assert_eq!( + ts.fixtures.metadata("test_file").permissions().mode() & 0o777, + 0o750 + ); +} + #[test] #[cfg(feature = "feat_selinux")] fn test_mknod_selinux() { From 227501ba8d7b61d6f50852d4c6a932ea6154778c Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Sun, 7 Dec 2025 05:38:36 +0900 Subject: [PATCH 173/182] build-gnu.sh: Remove 2 non-GNU binary --- util/build-gnu.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/util/build-gnu.sh b/util/build-gnu.sh index 223fc895b..ff29e119b 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -95,13 +95,13 @@ fi cd - # Pass the feature flags to make, which will pass them to cargo -"${MAKE}" PROFILE="${PROFILE}" CARGOFLAGS="${CARGO_FEATURE_FLAGS}" +"${MAKE}" PROFILE="${PROFILE}" SKIP_UTILS=more CARGOFLAGS="${CARGO_FEATURE_FLAGS}" # min test for SELinux [ "${SELINUX_ENABLED}" = 1 ] && touch g && "${PROFILE}"/stat -c%C g && rm g cp "${UU_BUILD_DIR}/install" "${UU_BUILD_DIR}/ginstall" # The GNU tests rename this script before running, to avoid confusion with the make target # Create *sum binaries -for sum in b2sum b3sum md5sum sha1sum sha224sum sha256sum sha384sum sha512sum; do +for sum in b2sum md5sum sha1sum sha224sum sha256sum sha384sum sha512sum; do sum_path="${UU_BUILD_DIR}/${sum}" test -f "${sum_path}" || (cd ${UU_BUILD_DIR} && ln -s "hashsum" "${sum}") done From 62962d35267bd9ca8054dface3bc898c25d2baed Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 7 Dec 2025 00:19:45 +0100 Subject: [PATCH 174/182] tee: fix poll timeout causing intermittent hangs with -p flag --- src/uu/tee/src/tee.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/uu/tee/src/tee.rs b/src/uu/tee/src/tee.rs index fc345a403..17afea938 100644 --- a/src/uu/tee/src/tee.rs +++ b/src/uu/tee/src/tee.rs @@ -443,11 +443,12 @@ pub fn ensure_stdout_not_broken() -> Result { // POLLRDBAND is the flag used by GNU tee. let mut pfds = [PollFd::new(out.as_fd(), PollFlags::POLLRDBAND)]; - // Then, ensure that the pipe is not broken - let res = nix::poll::poll(&mut pfds, PollTimeout::NONE)?; + // Then, ensure that the pipe is not broken. + // Use ZERO timeout to return immediately - we just want to check the current state. + let res = nix::poll::poll(&mut pfds, PollTimeout::ZERO)?; if res > 0 { - // poll succeeded; + // poll returned with events ready - check if POLLERR is set (pipe broken) let error = pfds.iter().any(|pfd| { if let Some(revents) = pfd.revents() { revents.contains(PollFlags::POLLERR) @@ -458,8 +459,8 @@ pub fn ensure_stdout_not_broken() -> Result { return Ok(!error); } - // if res == 0, it means that timeout was reached, which is impossible - // because we set infinite timeout. - // And if res < 0, the nix wrapper should have sent back an error. - unreachable!(); + // res == 0 means no events ready (timeout reached immediately with ZERO timeout). + // This means the pipe is healthy (not broken). + // res < 0 would be an error, but nix returns Err in that case. + Ok(true) } From 28576decc1c9f8015e14b744e963f0de560af996 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Sun, 7 Dec 2025 17:25:12 +0900 Subject: [PATCH 175/182] coreutils: Print utility not found to stderr --- src/common/validation.rs | 2 +- tests/test_util_name.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/common/validation.rs b/src/common/validation.rs index 1715ad4bf..61b832f35 100644 --- a/src/common/validation.rs +++ b/src/common/validation.rs @@ -25,7 +25,7 @@ pub fn get_all_utilities( /// Prints a "utility not found" error and exits pub fn not_found(util: &OsStr) -> ! { - println!("{}: function/utility not found", util.maybe_quote()); + eprintln!("{}: function/utility not found", util.maybe_quote()); process::exit(1); } diff --git a/tests/test_util_name.rs b/tests/test_util_name.rs index caf900db8..12309a6e3 100644 --- a/tests/test_util_name.rs +++ b/tests/test_util_name.rs @@ -195,9 +195,9 @@ fn util_invalid_name_invalid_command() { .unwrap(); let output = child.wait_with_output().unwrap(); assert_eq!(output.status.code(), Some(1)); - assert_eq!(output.stderr, b""); + assert_eq!(output.stdout, b""); assert_eq!( - output.stdout, + output.stderr, b"definitely_invalid: function/utility not found\n" ); } From a0d82777e5eb563224156d50415e67a34a5bcef9 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Sun, 7 Dec 2025 18:40:52 +0900 Subject: [PATCH 176/182] validation.rs: Remove non GNU hashsum aliases --- src/common/validation.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/common/validation.rs b/src/common/validation.rs index 1715ad4bf..6af2fe15d 100644 --- a/src/common/validation.rs +++ b/src/common/validation.rs @@ -51,9 +51,9 @@ fn get_canonical_util_name(util_name: &str) -> &str { "[" => "test", // hashsum aliases - all these hash commands are aliases for hashsum - "md5sum" | "sha1sum" | "sha224sum" | "sha256sum" | "sha384sum" | "sha512sum" - | "sha3sum" | "sha3-224sum" | "sha3-256sum" | "sha3-384sum" | "sha3-512sum" - | "shake128sum" | "shake256sum" | "b2sum" | "b3sum" => "hashsum", + "md5sum" | "sha1sum" | "sha224sum" | "sha256sum" | "sha384sum" | "sha512sum" | "b2sum" => { + "hashsum" + } "dir" => "ls", // dir is an alias for ls From 345f2ccd14d70572e4799416aebd74d38eb68d5a Mon Sep 17 00:00:00 2001 From: Mahdi Ali-Raihan Date: Sun, 7 Dec 2025 07:05:45 -0500 Subject: [PATCH 177/182] tests/mkfifo: added a test to check mkfifo permission denied error for code coverage (#9586) * tests/mkfifo: added a test to check mkfifo permission denied error for code coverage * fixed formatting --- tests/by-util/test_mkfifo.rs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/by-util/test_mkfifo.rs b/tests/by-util/test_mkfifo.rs index b90fc0c95..ac0b78b3a 100644 --- a/tests/by-util/test_mkfifo.rs +++ b/tests/by-util/test_mkfifo.rs @@ -126,6 +126,32 @@ fn test_create_fifo_with_umask() { test_fifo_creation(0o777, "p---------"); // spell-checker:disable-line } +#[test] +fn test_create_fifo_permission_denied() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let no_exec_dir = "owner_no_exec_dir"; + let named_pipe = "owner_no_exec_dir/mkfifo_err"; + + at.mkdir(no_exec_dir); + at.set_mode(no_exec_dir, 0o644); + + let err_msg = format!( + "mkfifo: cannot create fifo '{named_pipe}': File exists +mkfifo: cannot set permissions on '{named_pipe}': Permission denied (os error 13) +" + ); + + scene + .ucmd() + .arg(named_pipe) + .arg("-m") + .arg("666") + .fails() + .stderr_is(err_msg.as_str()); +} + #[test] #[cfg(feature = "feat_selinux")] fn test_mkfifo_selinux() { From ae7473483d42586c81ea5f82e7bc9e748e4f4ce1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 7 Dec 2025 12:06:28 +0000 Subject: [PATCH 178/182] chore(deps): update vmactions/freebsd-vm action to v1.2.9 --- .github/workflows/freebsd.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/freebsd.yml b/.github/workflows/freebsd.yml index ee1601f6b..84f6b55b2 100644 --- a/.github/workflows/freebsd.yml +++ b/.github/workflows/freebsd.yml @@ -43,7 +43,7 @@ jobs: with: disable_annotations: true - name: Prepare, build and test - uses: vmactions/freebsd-vm@v1.2.8 + uses: vmactions/freebsd-vm@v1.2.9 with: usesh: true sync: rsync @@ -139,7 +139,7 @@ jobs: with: disable_annotations: true - name: Prepare, build and test - uses: vmactions/freebsd-vm@v1.2.8 + uses: vmactions/freebsd-vm@v1.2.9 with: usesh: true sync: rsync From 39a8c87fd0b2a37697629d9c14db637fbcdaba74 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Sun, 7 Dec 2025 23:24:31 +0900 Subject: [PATCH 179/182] build-gnu.sh: Enable misc/coreutils.sh (#9572) * build-gnu.sh: Enable misc/coreutils.sh * why-error.md: Remove misc/coreutils.sh Co-authored-by: Daniel Hofstetter --------- Co-authored-by: Daniel Hofstetter --- util/build-gnu.sh | 6 ++++++ util/why-skip.md | 3 --- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/util/build-gnu.sh b/util/build-gnu.sh index ff29e119b..626400d6a 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -135,6 +135,7 @@ else ./bootstrap --skip-po # Use CFLAGS for best build time since we discard GNU coreutils CFLAGS="${CFLAGS} -pipe -O0 -s" ./configure --quiet --disable-gcc-warnings --disable-nls --disable-dependency-tracking --disable-bold-man-page-references \ + --enable-single-binary=symlinks \ "$([ "${SELINUX_ENABLED}" = 1 ] && echo --with-selinux || echo --without-selinux)" #Add timeout to to protect against hangs "${SED}" -i 's|^"\$@|'"${SYSTEM_TIMEOUT}"' 600 "\$@|' build-aux/test-driver @@ -169,6 +170,11 @@ grep -rl 'path_prepend_' tests/* | xargs -r "${SED}" -i 's| path_prepend_ ./src| # path_prepend_ sets $abs_path_dir_: set it manually instead. grep -rl '\$abs_path_dir_' tests/*/*.sh | xargs -r "${SED}" -i "s|\$abs_path_dir_|${UU_BUILD_DIR//\//\\/}|g" +# We use coreutils yes +"${SED}" -i "s|--coreutils-prog=||g" tests/misc/coreutils.sh +# Different message +"${SED}" -i "s|coreutils: unknown program 'blah'|blah: function/utility not found|" tests/misc/coreutils.sh + # Remove hfs dependency (should be merged to upstream) "${SED}" -i -e "s|hfsplus|ext4 -O casefold|" -e "s|cd mnt|rm -d mnt/lost+found;chattr +F mnt;cd mnt|" tests/mv/hardlink-case.sh diff --git a/util/why-skip.md b/util/why-skip.md index f179a58bb..b0c181944 100644 --- a/util/why-skip.md +++ b/util/why-skip.md @@ -44,9 +44,6 @@ = The Swedish locale with blank thousands separator is unavailable. = * tests/misc/sort-h-thousands-sep.sh -= multicall binary is disabled = -* tests/misc/coreutils.sh - = not running on GNU/Hurd = * tests/id/gnu-zero-uids.sh From 61e0eae8fc916763fc5acc5d52e7440bdcbc4d1b Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 7 Dec 2025 16:54:14 +0100 Subject: [PATCH 180/182] prepare version 0.5.0 --- Cargo.lock | 214 +++++++++++++++++++------------------- Cargo.toml | 216 +++++++++++++++++++-------------------- fuzz/Cargo.lock | 32 +++--- fuzz/uufuzz/Cargo.toml | 4 +- src/uu/stdbuf/Cargo.toml | 2 +- util/update-version.sh | 4 +- 6 files changed, 236 insertions(+), 236 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2b142f5a2..dae4e963c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -534,7 +534,7 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "coreutils" -version = "0.4.0" +version = "0.5.0" dependencies = [ "bincode", "chrono", @@ -3004,7 +3004,7 @@ dependencies = [ [[package]] name = "uu_arch" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3014,7 +3014,7 @@ dependencies = [ [[package]] name = "uu_base32" -version = "0.4.0" +version = "0.5.0" dependencies = [ "base64-simd", "clap", @@ -3024,7 +3024,7 @@ dependencies = [ [[package]] name = "uu_base64" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3036,7 +3036,7 @@ dependencies = [ [[package]] name = "uu_basename" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3045,7 +3045,7 @@ dependencies = [ [[package]] name = "uu_basenc" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3055,7 +3055,7 @@ dependencies = [ [[package]] name = "uu_cat" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3070,7 +3070,7 @@ dependencies = [ [[package]] name = "uu_chcon" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3083,7 +3083,7 @@ dependencies = [ [[package]] name = "uu_chgrp" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3092,7 +3092,7 @@ dependencies = [ [[package]] name = "uu_chmod" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3102,7 +3102,7 @@ dependencies = [ [[package]] name = "uu_chown" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3111,7 +3111,7 @@ dependencies = [ [[package]] name = "uu_chroot" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3121,7 +3121,7 @@ dependencies = [ [[package]] name = "uu_cksum" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3133,7 +3133,7 @@ dependencies = [ [[package]] name = "uu_comm" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3142,7 +3142,7 @@ dependencies = [ [[package]] name = "uu_cp" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3162,7 +3162,7 @@ dependencies = [ [[package]] name = "uu_csplit" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3173,7 +3173,7 @@ dependencies = [ [[package]] name = "uu_cut" -version = "0.4.0" +version = "0.5.0" dependencies = [ "bstr", "clap", @@ -3186,7 +3186,7 @@ dependencies = [ [[package]] name = "uu_date" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3199,7 +3199,7 @@ dependencies = [ [[package]] name = "uu_dd" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3213,7 +3213,7 @@ dependencies = [ [[package]] name = "uu_df" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3225,7 +3225,7 @@ dependencies = [ [[package]] name = "uu_dir" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "uu_ls", @@ -3234,7 +3234,7 @@ dependencies = [ [[package]] name = "uu_dircolors" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3243,7 +3243,7 @@ dependencies = [ [[package]] name = "uu_dirname" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3252,7 +3252,7 @@ dependencies = [ [[package]] name = "uu_du" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3266,7 +3266,7 @@ dependencies = [ [[package]] name = "uu_echo" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3275,7 +3275,7 @@ dependencies = [ [[package]] name = "uu_env" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3287,7 +3287,7 @@ dependencies = [ [[package]] name = "uu_expand" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3300,7 +3300,7 @@ dependencies = [ [[package]] name = "uu_expr" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3313,7 +3313,7 @@ dependencies = [ [[package]] name = "uu_factor" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3327,7 +3327,7 @@ dependencies = [ [[package]] name = "uu_false" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3336,7 +3336,7 @@ dependencies = [ [[package]] name = "uu_fmt" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3347,7 +3347,7 @@ dependencies = [ [[package]] name = "uu_fold" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3359,7 +3359,7 @@ dependencies = [ [[package]] name = "uu_groups" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3369,7 +3369,7 @@ dependencies = [ [[package]] name = "uu_hashsum" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3380,7 +3380,7 @@ dependencies = [ [[package]] name = "uu_head" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3391,7 +3391,7 @@ dependencies = [ [[package]] name = "uu_hostid" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3401,7 +3401,7 @@ dependencies = [ [[package]] name = "uu_hostname" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "dns-lookup", @@ -3413,7 +3413,7 @@ dependencies = [ [[package]] name = "uu_id" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3423,7 +3423,7 @@ dependencies = [ [[package]] name = "uu_install" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "file_diff", @@ -3436,7 +3436,7 @@ dependencies = [ [[package]] name = "uu_join" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3447,7 +3447,7 @@ dependencies = [ [[package]] name = "uu_kill" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3457,7 +3457,7 @@ dependencies = [ [[package]] name = "uu_link" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3466,7 +3466,7 @@ dependencies = [ [[package]] name = "uu_ln" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3476,7 +3476,7 @@ dependencies = [ [[package]] name = "uu_logname" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3486,7 +3486,7 @@ dependencies = [ [[package]] name = "uu_ls" -version = "0.4.0" +version = "0.5.0" dependencies = [ "ansi-width", "clap", @@ -3506,7 +3506,7 @@ dependencies = [ [[package]] name = "uu_mkdir" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3515,7 +3515,7 @@ dependencies = [ [[package]] name = "uu_mkfifo" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3525,7 +3525,7 @@ dependencies = [ [[package]] name = "uu_mknod" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3535,7 +3535,7 @@ dependencies = [ [[package]] name = "uu_mktemp" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3547,7 +3547,7 @@ dependencies = [ [[package]] name = "uu_more" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "crossterm", @@ -3559,7 +3559,7 @@ dependencies = [ [[package]] name = "uu_mv" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3575,7 +3575,7 @@ dependencies = [ [[package]] name = "uu_nice" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3586,7 +3586,7 @@ dependencies = [ [[package]] name = "uu_nl" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3598,7 +3598,7 @@ dependencies = [ [[package]] name = "uu_nohup" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3609,7 +3609,7 @@ dependencies = [ [[package]] name = "uu_nproc" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3619,7 +3619,7 @@ dependencies = [ [[package]] name = "uu_numfmt" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3631,7 +3631,7 @@ dependencies = [ [[package]] name = "uu_od" -version = "0.4.0" +version = "0.5.0" dependencies = [ "byteorder", "clap", @@ -3643,7 +3643,7 @@ dependencies = [ [[package]] name = "uu_paste" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3652,7 +3652,7 @@ dependencies = [ [[package]] name = "uu_pathchk" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3662,7 +3662,7 @@ dependencies = [ [[package]] name = "uu_pinky" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3671,7 +3671,7 @@ dependencies = [ [[package]] name = "uu_pr" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3683,7 +3683,7 @@ dependencies = [ [[package]] name = "uu_printenv" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3692,7 +3692,7 @@ dependencies = [ [[package]] name = "uu_printf" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3701,7 +3701,7 @@ dependencies = [ [[package]] name = "uu_ptx" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3712,7 +3712,7 @@ dependencies = [ [[package]] name = "uu_pwd" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3721,7 +3721,7 @@ dependencies = [ [[package]] name = "uu_readlink" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3730,7 +3730,7 @@ dependencies = [ [[package]] name = "uu_realpath" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3739,7 +3739,7 @@ dependencies = [ [[package]] name = "uu_rm" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3754,7 +3754,7 @@ dependencies = [ [[package]] name = "uu_rmdir" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3764,7 +3764,7 @@ dependencies = [ [[package]] name = "uu_runcon" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3776,7 +3776,7 @@ dependencies = [ [[package]] name = "uu_seq" -version = "0.4.0" +version = "0.5.0" dependencies = [ "bigdecimal", "clap", @@ -3791,7 +3791,7 @@ dependencies = [ [[package]] name = "uu_shred" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3802,7 +3802,7 @@ dependencies = [ [[package]] name = "uu_shuf" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3815,7 +3815,7 @@ dependencies = [ [[package]] name = "uu_sleep" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3824,7 +3824,7 @@ dependencies = [ [[package]] name = "uu_sort" -version = "0.4.0" +version = "0.5.0" dependencies = [ "bigdecimal", "binary-heap-plus", @@ -3848,7 +3848,7 @@ dependencies = [ [[package]] name = "uu_split" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3861,7 +3861,7 @@ dependencies = [ [[package]] name = "uu_stat" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3871,7 +3871,7 @@ dependencies = [ [[package]] name = "uu_stdbuf" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3883,7 +3883,7 @@ dependencies = [ [[package]] name = "uu_stdbuf_libstdbuf" -version = "0.4.0" +version = "0.5.0" dependencies = [ "ctor", "libc", @@ -3891,7 +3891,7 @@ dependencies = [ [[package]] name = "uu_stty" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3901,7 +3901,7 @@ dependencies = [ [[package]] name = "uu_sum" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3910,7 +3910,7 @@ dependencies = [ [[package]] name = "uu_sync" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3921,7 +3921,7 @@ dependencies = [ [[package]] name = "uu_tac" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3934,7 +3934,7 @@ dependencies = [ [[package]] name = "uu_tail" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3950,7 +3950,7 @@ dependencies = [ [[package]] name = "uu_tee" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3960,7 +3960,7 @@ dependencies = [ [[package]] name = "uu_test" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3972,7 +3972,7 @@ dependencies = [ [[package]] name = "uu_timeout" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3983,7 +3983,7 @@ dependencies = [ [[package]] name = "uu_touch" -version = "0.4.0" +version = "0.5.0" dependencies = [ "chrono", "clap", @@ -3998,7 +3998,7 @@ dependencies = [ [[package]] name = "uu_tr" -version = "0.4.0" +version = "0.5.0" dependencies = [ "bytecount", "clap", @@ -4009,7 +4009,7 @@ dependencies = [ [[package]] name = "uu_true" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -4018,7 +4018,7 @@ dependencies = [ [[package]] name = "uu_truncate" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -4027,7 +4027,7 @@ dependencies = [ [[package]] name = "uu_tsort" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -4039,7 +4039,7 @@ dependencies = [ [[package]] name = "uu_tty" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -4049,7 +4049,7 @@ dependencies = [ [[package]] name = "uu_uname" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -4059,7 +4059,7 @@ dependencies = [ [[package]] name = "uu_unexpand" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -4072,7 +4072,7 @@ dependencies = [ [[package]] name = "uu_uniq" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -4083,7 +4083,7 @@ dependencies = [ [[package]] name = "uu_unlink" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -4092,7 +4092,7 @@ dependencies = [ [[package]] name = "uu_uptime" -version = "0.4.0" +version = "0.5.0" dependencies = [ "chrono", "clap", @@ -4104,7 +4104,7 @@ dependencies = [ [[package]] name = "uu_users" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -4114,7 +4114,7 @@ dependencies = [ [[package]] name = "uu_vdir" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "uu_ls", @@ -4123,7 +4123,7 @@ dependencies = [ [[package]] name = "uu_wc" -version = "0.4.0" +version = "0.5.0" dependencies = [ "bytecount", "clap", @@ -4139,7 +4139,7 @@ dependencies = [ [[package]] name = "uu_who" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -4148,7 +4148,7 @@ dependencies = [ [[package]] name = "uu_whoami" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -4158,7 +4158,7 @@ dependencies = [ [[package]] name = "uu_yes" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -4169,7 +4169,7 @@ dependencies = [ [[package]] name = "uucore" -version = "0.4.0" +version = "0.5.0" dependencies = [ "base64-simd", "bigdecimal", @@ -4226,7 +4226,7 @@ dependencies = [ [[package]] name = "uucore_procs" -version = "0.4.0" +version = "0.5.0" dependencies = [ "proc-macro2", "quote", @@ -4244,7 +4244,7 @@ dependencies = [ [[package]] name = "uutests" -version = "0.4.0" +version = "0.5.0" dependencies = [ "ctor", "libc", diff --git a/Cargo.toml b/Cargo.toml index 79bff3955..7c44e64d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -292,7 +292,7 @@ homepage = "https://github.com/uutils/coreutils" keywords = ["coreutils", "uutils", "cross-platform", "cli", "utility"] license = "MIT" readme = "README.package.md" -version = "0.4.0" +version = "0.5.0" [workspace.dependencies] ansi-width = "0.1.0" @@ -398,11 +398,11 @@ fluent-bundle = "0.16.0" unic-langid = "0.9.6" fluent-syntax = "0.12.0" -uucore = { version = "0.4.0", package = "uucore", path = "src/uucore" } -uucore_procs = { version = "0.4.0", package = "uucore_procs", path = "src/uucore_procs" } -uu_ls = { version = "0.4.0", path = "src/uu/ls" } -uu_base32 = { version = "0.4.0", path = "src/uu/base32" } -uutests = { version = "0.4.0", package = "uutests", path = "tests/uutests" } +uucore = { version = "0.5.0", package = "uucore", path = "src/uucore" } +uucore_procs = { version = "0.5.0", package = "uucore_procs", path = "src/uucore_procs" } +uu_ls = { version = "0.5.0", path = "src/uu/ls" } +uu_base32 = { version = "0.5.0", path = "src/uu/base32" } +uutests = { version = "0.5.0", package = "uutests", path = "tests/uutests" } [dependencies] clap.workspace = true @@ -417,109 +417,109 @@ zip = { workspace = true, optional = true } # * uutils -uu_test = { optional = true, version = "0.4.0", package = "uu_test", path = "src/uu/test" } +uu_test = { optional = true, version = "0.5.0", package = "uu_test", path = "src/uu/test" } # -arch = { optional = true, version = "0.4.0", package = "uu_arch", path = "src/uu/arch" } -base32 = { optional = true, version = "0.4.0", package = "uu_base32", path = "src/uu/base32" } -base64 = { optional = true, version = "0.4.0", package = "uu_base64", path = "src/uu/base64" } -basename = { optional = true, version = "0.4.0", package = "uu_basename", path = "src/uu/basename" } -basenc = { optional = true, version = "0.4.0", package = "uu_basenc", path = "src/uu/basenc" } -cat = { optional = true, version = "0.4.0", package = "uu_cat", path = "src/uu/cat" } -chcon = { optional = true, version = "0.4.0", package = "uu_chcon", path = "src/uu/chcon" } -chgrp = { optional = true, version = "0.4.0", package = "uu_chgrp", path = "src/uu/chgrp" } -chmod = { optional = true, version = "0.4.0", package = "uu_chmod", path = "src/uu/chmod" } -chown = { optional = true, version = "0.4.0", package = "uu_chown", path = "src/uu/chown" } -chroot = { optional = true, version = "0.4.0", package = "uu_chroot", path = "src/uu/chroot" } -cksum = { optional = true, version = "0.4.0", package = "uu_cksum", path = "src/uu/cksum" } -comm = { optional = true, version = "0.4.0", package = "uu_comm", path = "src/uu/comm" } -cp = { optional = true, version = "0.4.0", package = "uu_cp", path = "src/uu/cp" } -csplit = { optional = true, version = "0.4.0", package = "uu_csplit", path = "src/uu/csplit" } -cut = { optional = true, version = "0.4.0", package = "uu_cut", path = "src/uu/cut" } -date = { optional = true, version = "0.4.0", package = "uu_date", path = "src/uu/date" } -dd = { optional = true, version = "0.4.0", package = "uu_dd", path = "src/uu/dd" } -df = { optional = true, version = "0.4.0", package = "uu_df", path = "src/uu/df" } -dir = { optional = true, version = "0.4.0", package = "uu_dir", path = "src/uu/dir" } -dircolors = { optional = true, version = "0.4.0", package = "uu_dircolors", path = "src/uu/dircolors" } -dirname = { optional = true, version = "0.4.0", package = "uu_dirname", path = "src/uu/dirname" } -du = { optional = true, version = "0.4.0", package = "uu_du", path = "src/uu/du" } -echo = { optional = true, version = "0.4.0", package = "uu_echo", path = "src/uu/echo" } -env = { optional = true, version = "0.4.0", package = "uu_env", path = "src/uu/env" } -expand = { optional = true, version = "0.4.0", package = "uu_expand", path = "src/uu/expand" } -expr = { optional = true, version = "0.4.0", package = "uu_expr", path = "src/uu/expr" } -factor = { optional = true, version = "0.4.0", package = "uu_factor", path = "src/uu/factor" } -false = { optional = true, version = "0.4.0", package = "uu_false", path = "src/uu/false" } -fmt = { optional = true, version = "0.4.0", package = "uu_fmt", path = "src/uu/fmt" } -fold = { optional = true, version = "0.4.0", package = "uu_fold", path = "src/uu/fold" } -groups = { optional = true, version = "0.4.0", package = "uu_groups", path = "src/uu/groups" } -hashsum = { optional = true, version = "0.4.0", package = "uu_hashsum", path = "src/uu/hashsum" } -head = { optional = true, version = "0.4.0", package = "uu_head", path = "src/uu/head" } -hostid = { optional = true, version = "0.4.0", package = "uu_hostid", path = "src/uu/hostid" } -hostname = { optional = true, version = "0.4.0", package = "uu_hostname", path = "src/uu/hostname" } -id = { optional = true, version = "0.4.0", package = "uu_id", path = "src/uu/id" } -install = { optional = true, version = "0.4.0", package = "uu_install", path = "src/uu/install" } -join = { optional = true, version = "0.4.0", package = "uu_join", path = "src/uu/join" } -kill = { optional = true, version = "0.4.0", package = "uu_kill", path = "src/uu/kill" } -link = { optional = true, version = "0.4.0", package = "uu_link", path = "src/uu/link" } -ln = { optional = true, version = "0.4.0", package = "uu_ln", path = "src/uu/ln" } -ls = { optional = true, version = "0.4.0", package = "uu_ls", path = "src/uu/ls" } -logname = { optional = true, version = "0.4.0", package = "uu_logname", path = "src/uu/logname" } -mkdir = { optional = true, version = "0.4.0", package = "uu_mkdir", path = "src/uu/mkdir" } -mkfifo = { optional = true, version = "0.4.0", package = "uu_mkfifo", path = "src/uu/mkfifo" } -mknod = { optional = true, version = "0.4.0", package = "uu_mknod", path = "src/uu/mknod" } -mktemp = { optional = true, version = "0.4.0", package = "uu_mktemp", path = "src/uu/mktemp" } -more = { optional = true, version = "0.4.0", package = "uu_more", path = "src/uu/more" } -mv = { optional = true, version = "0.4.0", package = "uu_mv", path = "src/uu/mv" } -nice = { optional = true, version = "0.4.0", package = "uu_nice", path = "src/uu/nice" } -nl = { optional = true, version = "0.4.0", package = "uu_nl", path = "src/uu/nl" } -nohup = { optional = true, version = "0.4.0", package = "uu_nohup", path = "src/uu/nohup" } -nproc = { optional = true, version = "0.4.0", package = "uu_nproc", path = "src/uu/nproc" } -numfmt = { optional = true, version = "0.4.0", package = "uu_numfmt", path = "src/uu/numfmt" } -od = { optional = true, version = "0.4.0", package = "uu_od", path = "src/uu/od" } -paste = { optional = true, version = "0.4.0", package = "uu_paste", path = "src/uu/paste" } -pathchk = { optional = true, version = "0.4.0", package = "uu_pathchk", path = "src/uu/pathchk" } -pinky = { optional = true, version = "0.4.0", package = "uu_pinky", path = "src/uu/pinky" } -pr = { optional = true, version = "0.4.0", package = "uu_pr", path = "src/uu/pr" } -printenv = { optional = true, version = "0.4.0", package = "uu_printenv", path = "src/uu/printenv" } -printf = { optional = true, version = "0.4.0", package = "uu_printf", path = "src/uu/printf" } -ptx = { optional = true, version = "0.4.0", package = "uu_ptx", path = "src/uu/ptx" } -pwd = { optional = true, version = "0.4.0", package = "uu_pwd", path = "src/uu/pwd" } -readlink = { optional = true, version = "0.4.0", package = "uu_readlink", path = "src/uu/readlink" } -realpath = { optional = true, version = "0.4.0", package = "uu_realpath", path = "src/uu/realpath" } -rm = { optional = true, version = "0.4.0", package = "uu_rm", path = "src/uu/rm" } -rmdir = { optional = true, version = "0.4.0", package = "uu_rmdir", path = "src/uu/rmdir" } -runcon = { optional = true, version = "0.4.0", package = "uu_runcon", path = "src/uu/runcon" } -seq = { optional = true, version = "0.4.0", package = "uu_seq", path = "src/uu/seq" } -shred = { optional = true, version = "0.4.0", package = "uu_shred", path = "src/uu/shred" } -shuf = { optional = true, version = "0.4.0", package = "uu_shuf", path = "src/uu/shuf" } -sleep = { optional = true, version = "0.4.0", package = "uu_sleep", path = "src/uu/sleep" } -sort = { optional = true, version = "0.4.0", package = "uu_sort", path = "src/uu/sort" } -split = { optional = true, version = "0.4.0", package = "uu_split", path = "src/uu/split" } -stat = { optional = true, version = "0.4.0", package = "uu_stat", path = "src/uu/stat" } -stdbuf = { optional = true, version = "0.4.0", package = "uu_stdbuf", path = "src/uu/stdbuf" } -stty = { optional = true, version = "0.4.0", package = "uu_stty", path = "src/uu/stty" } -sum = { optional = true, version = "0.4.0", package = "uu_sum", path = "src/uu/sum" } -sync = { optional = true, version = "0.4.0", package = "uu_sync", path = "src/uu/sync" } -tac = { optional = true, version = "0.4.0", package = "uu_tac", path = "src/uu/tac" } -tail = { optional = true, version = "0.4.0", package = "uu_tail", path = "src/uu/tail" } -tee = { optional = true, version = "0.4.0", package = "uu_tee", path = "src/uu/tee" } -timeout = { optional = true, version = "0.4.0", package = "uu_timeout", path = "src/uu/timeout" } -touch = { optional = true, version = "0.4.0", package = "uu_touch", path = "src/uu/touch" } -tr = { optional = true, version = "0.4.0", package = "uu_tr", path = "src/uu/tr" } -true = { optional = true, version = "0.4.0", package = "uu_true", path = "src/uu/true" } -truncate = { optional = true, version = "0.4.0", package = "uu_truncate", path = "src/uu/truncate" } -tsort = { optional = true, version = "0.4.0", package = "uu_tsort", path = "src/uu/tsort" } -tty = { optional = true, version = "0.4.0", package = "uu_tty", path = "src/uu/tty" } -uname = { optional = true, version = "0.4.0", package = "uu_uname", path = "src/uu/uname" } -unexpand = { optional = true, version = "0.4.0", package = "uu_unexpand", path = "src/uu/unexpand" } -uniq = { optional = true, version = "0.4.0", package = "uu_uniq", path = "src/uu/uniq" } -unlink = { optional = true, version = "0.4.0", package = "uu_unlink", path = "src/uu/unlink" } -uptime = { optional = true, version = "0.4.0", package = "uu_uptime", path = "src/uu/uptime" } -users = { optional = true, version = "0.4.0", package = "uu_users", path = "src/uu/users" } -vdir = { optional = true, version = "0.4.0", package = "uu_vdir", path = "src/uu/vdir" } -wc = { optional = true, version = "0.4.0", package = "uu_wc", path = "src/uu/wc" } -who = { optional = true, version = "0.4.0", package = "uu_who", path = "src/uu/who" } -whoami = { optional = true, version = "0.4.0", package = "uu_whoami", path = "src/uu/whoami" } -yes = { optional = true, version = "0.4.0", package = "uu_yes", path = "src/uu/yes" } +arch = { optional = true, version = "0.5.0", package = "uu_arch", path = "src/uu/arch" } +base32 = { optional = true, version = "0.5.0", package = "uu_base32", path = "src/uu/base32" } +base64 = { optional = true, version = "0.5.0", package = "uu_base64", path = "src/uu/base64" } +basename = { optional = true, version = "0.5.0", package = "uu_basename", path = "src/uu/basename" } +basenc = { optional = true, version = "0.5.0", package = "uu_basenc", path = "src/uu/basenc" } +cat = { optional = true, version = "0.5.0", package = "uu_cat", path = "src/uu/cat" } +chcon = { optional = true, version = "0.5.0", package = "uu_chcon", path = "src/uu/chcon" } +chgrp = { optional = true, version = "0.5.0", package = "uu_chgrp", path = "src/uu/chgrp" } +chmod = { optional = true, version = "0.5.0", package = "uu_chmod", path = "src/uu/chmod" } +chown = { optional = true, version = "0.5.0", package = "uu_chown", path = "src/uu/chown" } +chroot = { optional = true, version = "0.5.0", package = "uu_chroot", path = "src/uu/chroot" } +cksum = { optional = true, version = "0.5.0", package = "uu_cksum", path = "src/uu/cksum" } +comm = { optional = true, version = "0.5.0", package = "uu_comm", path = "src/uu/comm" } +cp = { optional = true, version = "0.5.0", package = "uu_cp", path = "src/uu/cp" } +csplit = { optional = true, version = "0.5.0", package = "uu_csplit", path = "src/uu/csplit" } +cut = { optional = true, version = "0.5.0", package = "uu_cut", path = "src/uu/cut" } +date = { optional = true, version = "0.5.0", package = "uu_date", path = "src/uu/date" } +dd = { optional = true, version = "0.5.0", package = "uu_dd", path = "src/uu/dd" } +df = { optional = true, version = "0.5.0", package = "uu_df", path = "src/uu/df" } +dir = { optional = true, version = "0.5.0", package = "uu_dir", path = "src/uu/dir" } +dircolors = { optional = true, version = "0.5.0", package = "uu_dircolors", path = "src/uu/dircolors" } +dirname = { optional = true, version = "0.5.0", package = "uu_dirname", path = "src/uu/dirname" } +du = { optional = true, version = "0.5.0", package = "uu_du", path = "src/uu/du" } +echo = { optional = true, version = "0.5.0", package = "uu_echo", path = "src/uu/echo" } +env = { optional = true, version = "0.5.0", package = "uu_env", path = "src/uu/env" } +expand = { optional = true, version = "0.5.0", package = "uu_expand", path = "src/uu/expand" } +expr = { optional = true, version = "0.5.0", package = "uu_expr", path = "src/uu/expr" } +factor = { optional = true, version = "0.5.0", package = "uu_factor", path = "src/uu/factor" } +false = { optional = true, version = "0.5.0", package = "uu_false", path = "src/uu/false" } +fmt = { optional = true, version = "0.5.0", package = "uu_fmt", path = "src/uu/fmt" } +fold = { optional = true, version = "0.5.0", package = "uu_fold", path = "src/uu/fold" } +groups = { optional = true, version = "0.5.0", package = "uu_groups", path = "src/uu/groups" } +hashsum = { optional = true, version = "0.5.0", package = "uu_hashsum", path = "src/uu/hashsum" } +head = { optional = true, version = "0.5.0", package = "uu_head", path = "src/uu/head" } +hostid = { optional = true, version = "0.5.0", package = "uu_hostid", path = "src/uu/hostid" } +hostname = { optional = true, version = "0.5.0", package = "uu_hostname", path = "src/uu/hostname" } +id = { optional = true, version = "0.5.0", package = "uu_id", path = "src/uu/id" } +install = { optional = true, version = "0.5.0", package = "uu_install", path = "src/uu/install" } +join = { optional = true, version = "0.5.0", package = "uu_join", path = "src/uu/join" } +kill = { optional = true, version = "0.5.0", package = "uu_kill", path = "src/uu/kill" } +link = { optional = true, version = "0.5.0", package = "uu_link", path = "src/uu/link" } +ln = { optional = true, version = "0.5.0", package = "uu_ln", path = "src/uu/ln" } +ls = { optional = true, version = "0.5.0", package = "uu_ls", path = "src/uu/ls" } +logname = { optional = true, version = "0.5.0", package = "uu_logname", path = "src/uu/logname" } +mkdir = { optional = true, version = "0.5.0", package = "uu_mkdir", path = "src/uu/mkdir" } +mkfifo = { optional = true, version = "0.5.0", package = "uu_mkfifo", path = "src/uu/mkfifo" } +mknod = { optional = true, version = "0.5.0", package = "uu_mknod", path = "src/uu/mknod" } +mktemp = { optional = true, version = "0.5.0", package = "uu_mktemp", path = "src/uu/mktemp" } +more = { optional = true, version = "0.5.0", package = "uu_more", path = "src/uu/more" } +mv = { optional = true, version = "0.5.0", package = "uu_mv", path = "src/uu/mv" } +nice = { optional = true, version = "0.5.0", package = "uu_nice", path = "src/uu/nice" } +nl = { optional = true, version = "0.5.0", package = "uu_nl", path = "src/uu/nl" } +nohup = { optional = true, version = "0.5.0", package = "uu_nohup", path = "src/uu/nohup" } +nproc = { optional = true, version = "0.5.0", package = "uu_nproc", path = "src/uu/nproc" } +numfmt = { optional = true, version = "0.5.0", package = "uu_numfmt", path = "src/uu/numfmt" } +od = { optional = true, version = "0.5.0", package = "uu_od", path = "src/uu/od" } +paste = { optional = true, version = "0.5.0", package = "uu_paste", path = "src/uu/paste" } +pathchk = { optional = true, version = "0.5.0", package = "uu_pathchk", path = "src/uu/pathchk" } +pinky = { optional = true, version = "0.5.0", package = "uu_pinky", path = "src/uu/pinky" } +pr = { optional = true, version = "0.5.0", package = "uu_pr", path = "src/uu/pr" } +printenv = { optional = true, version = "0.5.0", package = "uu_printenv", path = "src/uu/printenv" } +printf = { optional = true, version = "0.5.0", package = "uu_printf", path = "src/uu/printf" } +ptx = { optional = true, version = "0.5.0", package = "uu_ptx", path = "src/uu/ptx" } +pwd = { optional = true, version = "0.5.0", package = "uu_pwd", path = "src/uu/pwd" } +readlink = { optional = true, version = "0.5.0", package = "uu_readlink", path = "src/uu/readlink" } +realpath = { optional = true, version = "0.5.0", package = "uu_realpath", path = "src/uu/realpath" } +rm = { optional = true, version = "0.5.0", package = "uu_rm", path = "src/uu/rm" } +rmdir = { optional = true, version = "0.5.0", package = "uu_rmdir", path = "src/uu/rmdir" } +runcon = { optional = true, version = "0.5.0", package = "uu_runcon", path = "src/uu/runcon" } +seq = { optional = true, version = "0.5.0", package = "uu_seq", path = "src/uu/seq" } +shred = { optional = true, version = "0.5.0", package = "uu_shred", path = "src/uu/shred" } +shuf = { optional = true, version = "0.5.0", package = "uu_shuf", path = "src/uu/shuf" } +sleep = { optional = true, version = "0.5.0", package = "uu_sleep", path = "src/uu/sleep" } +sort = { optional = true, version = "0.5.0", package = "uu_sort", path = "src/uu/sort" } +split = { optional = true, version = "0.5.0", package = "uu_split", path = "src/uu/split" } +stat = { optional = true, version = "0.5.0", package = "uu_stat", path = "src/uu/stat" } +stdbuf = { optional = true, version = "0.5.0", package = "uu_stdbuf", path = "src/uu/stdbuf" } +stty = { optional = true, version = "0.5.0", package = "uu_stty", path = "src/uu/stty" } +sum = { optional = true, version = "0.5.0", package = "uu_sum", path = "src/uu/sum" } +sync = { optional = true, version = "0.5.0", package = "uu_sync", path = "src/uu/sync" } +tac = { optional = true, version = "0.5.0", package = "uu_tac", path = "src/uu/tac" } +tail = { optional = true, version = "0.5.0", package = "uu_tail", path = "src/uu/tail" } +tee = { optional = true, version = "0.5.0", package = "uu_tee", path = "src/uu/tee" } +timeout = { optional = true, version = "0.5.0", package = "uu_timeout", path = "src/uu/timeout" } +touch = { optional = true, version = "0.5.0", package = "uu_touch", path = "src/uu/touch" } +tr = { optional = true, version = "0.5.0", package = "uu_tr", path = "src/uu/tr" } +true = { optional = true, version = "0.5.0", package = "uu_true", path = "src/uu/true" } +truncate = { optional = true, version = "0.5.0", package = "uu_truncate", path = "src/uu/truncate" } +tsort = { optional = true, version = "0.5.0", package = "uu_tsort", path = "src/uu/tsort" } +tty = { optional = true, version = "0.5.0", package = "uu_tty", path = "src/uu/tty" } +uname = { optional = true, version = "0.5.0", package = "uu_uname", path = "src/uu/uname" } +unexpand = { optional = true, version = "0.5.0", package = "uu_unexpand", path = "src/uu/unexpand" } +uniq = { optional = true, version = "0.5.0", package = "uu_uniq", path = "src/uu/uniq" } +unlink = { optional = true, version = "0.5.0", package = "uu_unlink", path = "src/uu/unlink" } +uptime = { optional = true, version = "0.5.0", package = "uu_uptime", path = "src/uu/uptime" } +users = { optional = true, version = "0.5.0", package = "uu_users", path = "src/uu/users" } +vdir = { optional = true, version = "0.5.0", package = "uu_vdir", path = "src/uu/vdir" } +wc = { optional = true, version = "0.5.0", package = "uu_wc", path = "src/uu/wc" } +who = { optional = true, version = "0.5.0", package = "uu_who", path = "src/uu/who" } +whoami = { optional = true, version = "0.5.0", package = "uu_whoami", path = "src/uu/whoami" } +yes = { optional = true, version = "0.5.0", package = "uu_yes", path = "src/uu/yes" } # this breaks clippy linting with: "tests/by-util/test_factor_benches.rs: No such file or directory (os error 2)" # factor_benches = { optional = true, version = "0.0.0", package = "uu_factor_benches", path = "tests/benches/factor" } diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index 989bce43f..ccb71eaff 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -1571,7 +1571,7 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uu_cksum" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -1581,7 +1581,7 @@ dependencies = [ [[package]] name = "uu_cut" -version = "0.4.0" +version = "0.5.0" dependencies = [ "bstr", "clap", @@ -1592,7 +1592,7 @@ dependencies = [ [[package]] name = "uu_date" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -1605,7 +1605,7 @@ dependencies = [ [[package]] name = "uu_echo" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -1614,7 +1614,7 @@ dependencies = [ [[package]] name = "uu_env" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -1626,7 +1626,7 @@ dependencies = [ [[package]] name = "uu_expr" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -1639,7 +1639,7 @@ dependencies = [ [[package]] name = "uu_printf" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -1648,7 +1648,7 @@ dependencies = [ [[package]] name = "uu_seq" -version = "0.4.0" +version = "0.5.0" dependencies = [ "bigdecimal", "clap", @@ -1661,7 +1661,7 @@ dependencies = [ [[package]] name = "uu_sort" -version = "0.4.0" +version = "0.5.0" dependencies = [ "bigdecimal", "binary-heap-plus", @@ -1684,7 +1684,7 @@ dependencies = [ [[package]] name = "uu_split" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -1695,7 +1695,7 @@ dependencies = [ [[package]] name = "uu_test" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -1706,7 +1706,7 @@ dependencies = [ [[package]] name = "uu_tr" -version = "0.4.0" +version = "0.5.0" dependencies = [ "bytecount", "clap", @@ -1717,7 +1717,7 @@ dependencies = [ [[package]] name = "uu_wc" -version = "0.4.0" +version = "0.5.0" dependencies = [ "bytecount", "clap", @@ -1731,7 +1731,7 @@ dependencies = [ [[package]] name = "uucore" -version = "0.4.0" +version = "0.5.0" dependencies = [ "base64-simd", "bigdecimal", @@ -1798,7 +1798,7 @@ dependencies = [ [[package]] name = "uucore_procs" -version = "0.4.0" +version = "0.5.0" dependencies = [ "proc-macro2", "quote", @@ -1806,7 +1806,7 @@ dependencies = [ [[package]] name = "uufuzz" -version = "0.4.0" +version = "0.5.0" dependencies = [ "console", "libc", diff --git a/fuzz/uufuzz/Cargo.toml b/fuzz/uufuzz/Cargo.toml index 2a5abeee4..c68bcb428 100644 --- a/fuzz/uufuzz/Cargo.toml +++ b/fuzz/uufuzz/Cargo.toml @@ -3,7 +3,7 @@ name = "uufuzz" authors = ["uutils developers"] description = "uutils ~ 'core' uutils fuzzing library" repository = "https://github.com/uutils/coreutils/tree/main/fuzz/uufuzz" -version = "0.4.0" +version = "0.5.0" edition.workspace = true license.workspace = true @@ -12,5 +12,5 @@ console = "0.16.0" libc = "0.2.153" rand = { version = "0.9.0", features = ["small_rng"] } similar = "2.5.0" -uucore = { version = "0.4.0", path = "../../src/uucore", features = ["parser"] } +uucore = { version = "0.5.0", path = "../../src/uucore", features = ["parser"] } tempfile = "3.15.0" diff --git a/src/uu/stdbuf/Cargo.toml b/src/uu/stdbuf/Cargo.toml index cb5445026..41940f2df 100644 --- a/src/uu/stdbuf/Cargo.toml +++ b/src/uu/stdbuf/Cargo.toml @@ -20,7 +20,7 @@ path = "src/stdbuf.rs" [dependencies] clap = { workspace = true } -libstdbuf = { package = "uu_stdbuf_libstdbuf", version = "0.4.0", path = "src/libstdbuf" } +libstdbuf = { package = "uu_stdbuf_libstdbuf", version = "0.5.0", path = "src/libstdbuf" } tempfile = { workspace = true } uucore = { workspace = true, features = ["parser-size"] } thiserror = { workspace = true } diff --git a/util/update-version.sh b/util/update-version.sh index ae28cf4f1..9b89937ab 100755 --- a/util/update-version.sh +++ b/util/update-version.sh @@ -17,8 +17,8 @@ # 10) Create the release on github https://github.com/uutils/coreutils/releases/new # 11) Make sure we have good release notes -FROM="0.3.0" -TO="0.4.0" +FROM="0.4.0" +TO="0.5.0" PROGS=$(ls -1d src/uu/*/Cargo.toml src/uu/stdbuf/src/libstdbuf/Cargo.toml src/uucore/Cargo.toml Cargo.toml fuzz/uufuzz/Cargo.toml src/uu/stdbuf/Cargo.toml) From a7c9d03ea394b944e6430d3fe6cdc18bf12d3dbe Mon Sep 17 00:00:00 2001 From: Shay Elkin <2046772+shayelkin@users.noreply.github.com> Date: Sun, 7 Dec 2025 08:07:19 -0800 Subject: [PATCH 181/182] Merge pull request #9152 from shayelkin/main uudoc: fix manpage for individual utilities has wrong name (nit) --- src/bin/uudoc.rs | 1 + tests/uudoc/mod.rs | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/bin/uudoc.rs b/src/bin/uudoc.rs index a454555b3..689e26020 100644 --- a/src/bin/uudoc.rs +++ b/src/bin/uudoc.rs @@ -66,6 +66,7 @@ fn gen_manpage( args: impl Iterator, util_map: &UtilityMap, ) -> ! { + uucore::set_utility_is_second_arg(); let all_utilities = validation::get_all_utilities(util_map); let matches = Command::new("manpage") diff --git a/tests/uudoc/mod.rs b/tests/uudoc/mod.rs index fc64417e4..4be9803b8 100644 --- a/tests/uudoc/mod.rs +++ b/tests/uudoc/mod.rs @@ -34,8 +34,9 @@ fn test_manpage_generation() { ); let output_str = String::from_utf8_lossy(&output.stdout); - assert!(output_str.contains("\n.TH"), "{output_str}"); + assert!(output_str.contains("\n.TH ls"), "{output_str}"); assert!(output_str.contains('1'), "{output_str}"); + assert!(output_str.contains("\n.SH NAME\nls"), "{output_str}"); } #[test] @@ -57,8 +58,9 @@ fn test_manpage_coreutils() { ); let output_str = String::from_utf8_lossy(&output.stdout); - assert!(output_str.contains("\n.TH"), "{output_str}"); + assert!(output_str.contains("\n.TH coreutils"), "{output_str}"); assert!(output_str.contains("coreutils"), "{output_str}"); + assert!(output_str.contains("\n.SH NAME\ncoreutils"), "{output_str}"); } #[test] From 3528d106e4b9930df9389eef2d91d41b8b9fb80e Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Mon, 8 Dec 2025 01:07:44 +0900 Subject: [PATCH 182/182] GHA-delete-GNU-workflow-logs.sh: Support custom jq command and support jaq for the case it is not installed as jq (#9581) --- util/GHA-delete-GNU-workflow-logs.sh | 31 ++++++++-------------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/util/GHA-delete-GNU-workflow-logs.sh b/util/GHA-delete-GNU-workflow-logs.sh index ebeb7cfe8..95f5c240e 100755 --- a/util/GHA-delete-GNU-workflow-logs.sh +++ b/util/GHA-delete-GNU-workflow-logs.sh @@ -1,6 +1,6 @@ #!/bin/sh -# spell-checker:ignore (utils) gitsome jq ; (gh) repos +# spell-checker:ignore (utils) gitsome jq jaq ; (gh) repos # ME="${0}" # ME_dir="$(dirname -- "${ME}")" @@ -14,24 +14,11 @@ ## tools available? # * `gh` available? -unset GH -if gh --version 1>/dev/null 2>&1; then - export GH="gh" -else - echo "ERR!: missing \`gh\` (see install instructions at )" 1>&2 -fi - -# * `jq` available? -unset JQ -if jq --version 1>/dev/null 2>&1; then - export JQ="jq" -else - echo "ERR!: missing \`jq\` (install with \`sudo apt install jq\`)" 1>&2 -fi - -if [ -z "${GH}" ] || [ -z "${JQ}" ]; then - exit 1 -fi +GH=$(command -v gh) +"${GH}" --version || (echo "ERR!: missing \`gh\` (see install instructions at )"; exit 1) +# * `jq` or fallback available? +: ${JQ:=$(command -v jq || command -v jaq)} +"${JQ}" --version || (echo "ERR!: missing \`jq\` (install with \`sudo apt install jq\`)"; exit 1) case "${dry_run}" in '0' | 'f' | 'false' | 'no' | 'never' | 'none') unset dry_run ;; @@ -44,6 +31,6 @@ WORK_NAME="${WORK_NAME:-GNU}" # * `--paginate` retrieves all pages # gh api --paginate "repos/${USER_NAME}/${REPO_NAME}/actions/runs" | jq -r ".workflow_runs[] | select(.name == \"${WORK_NAME}\") | (.id)" | xargs -n1 sh -c "for arg do { echo gh api repos/${USER_NAME}/${REPO_NAME}/actions/runs/\${arg} -X DELETE ; if [ -z "$dry_run" ]; then gh api repos/${USER_NAME}/${REPO_NAME}/actions/runs/\${arg} -X DELETE ; fi ; } ; done ;" _ -gh api "repos/${USER_NAME}/${REPO_NAME}/actions/runs" | - jq -r ".workflow_runs[] | select(.name == \"${WORK_NAME}\") | (.id)" | - xargs -n1 sh -c "for arg do { echo gh api repos/${USER_NAME}/${REPO_NAME}/actions/runs/\${arg} -X DELETE ; if [ -z \"${dry_run}\" ]; then gh api repos/${USER_NAME}/${REPO_NAME}/actions/runs/\${arg} -X DELETE ; fi ; } ; done ;" _ +"${GH}" api "repos/${USER_NAME}/${REPO_NAME}/actions/runs" | + "${JQ}" -r ".workflow_runs[] | select(.name == \"${WORK_NAME}\") | (.id)" | + xargs -n1 sh -c "for arg do { echo ${GH} api repos/${USER_NAME}/${REPO_NAME}/actions/runs/\${arg} -X DELETE ; if [ -z \"${dry_run}\" ]; then ${GH} api repos/${USER_NAME}/${REPO_NAME}/actions/runs/\${arg} -X DELETE ; fi ; } ; done ;" _