diff --git a/.cargo/config.toml b/.cargo/config.toml index 364776950c7..803f6249978 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -6,6 +6,8 @@ linker = "x86_64-unknown-redox-gcc" [target.aarch64-unknown-linux-gnu] linker = "aarch64-linux-gnu-gcc" +[target.riscv64gc-unknown-linux-musl] +rustflags = ["-C", "target-feature=+crt-static"] [env] # See feat_external_libstdbuf in src/uu/stdbuf/Cargo.toml diff --git a/.config/nextest.toml b/.config/nextest.toml index 473c461402a..710ff26c59d 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -5,9 +5,15 @@ final-status-level = "skip" failure-output = "immediate-final" fail-fast = false +[profile.ci.junit] +path = "junit.xml" + [profile.coverage] retries = 0 status-level = "all" final-status-level = "skip" failure-output = "immediate-final" fail-fast = false + +[profile.coverage.junit] +path = "junit.xml" diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index f0d2c848220..2f8e1035bfe 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -228,6 +228,16 @@ jobs: env: RUSTFLAGS: "-Awarnings" RUST_BACKTRACE: "1" + - name: Upload test results to Codecov + if: ${{ !cancelled() }} + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + report_type: test_results + files: target/nextest/ci/junit.xml + disable_search: true + flags: msrv,${{ matrix.job.os }} + fail_ci_if_error: false deps: name: Dependencies @@ -300,6 +310,16 @@ jobs: run: make nextest PROFILE=ci CARGOFLAGS="--hide-progress-bar" env: RUST_BACKTRACE: "1" + - name: Upload test results to Codecov + if: ${{ !cancelled() }} + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + report_type: test_results + files: target/nextest/ci/junit.xml + disable_search: true + flags: makefile,${{ matrix.job.os }} + fail_ci_if_error: false - name: "`make install PROG_PREFIX=uu- PROFILE=release-fast COMPLETIONS=n MANPAGES=n LOCALES=n`" shell: bash run: | @@ -309,7 +329,7 @@ jobs: ./target/release-fast/true # Check that the progs have prefix test -f /tmp/usr/local/bin/uu-tty - test -f /tmp/usr/local/libexec/uu-coreutils/libstdbuf.* + test -f /tmp/usr/local/libexec/uu-coreutils/libstdbuf.* # Check that the manpage is not present ! test -f /tmp/usr/local/share/man/man1/uu-whoami.1 # Check that the completion is not present @@ -410,6 +430,16 @@ jobs: run: cargo nextest run --hide-progress-bar --profile ci --features ${{ matrix.job.features }} env: RUST_BACKTRACE: "1" + - name: Upload test results to Codecov + if: ${{ !cancelled() }} + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + report_type: test_results + files: target/nextest/ci/junit.xml + disable_search: true + flags: stable,${{ matrix.job.os }} + fail_ci_if_error: false build_rust_nightly: name: Build/nightly @@ -439,6 +469,16 @@ jobs: run: cargo nextest run --hide-progress-bar --profile ci --features ${{ matrix.job.features }} env: RUST_BACKTRACE: "1" + - name: Upload test results to Codecov + if: ${{ !cancelled() }} + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + report_type: test_results + files: target/nextest/ci/junit.xml + disable_search: true + flags: nightly,${{ matrix.job.os }} + fail_ci_if_error: false compute_size: name: Binary sizes @@ -576,9 +616,12 @@ jobs: - { os: ubuntu-latest , target: arm-unknown-linux-gnueabihf , features: feat_os_unix_gnueabihf , use-cross: use-cross , skip-tests: true } - { os: ubuntu-24.04-arm , target: aarch64-unknown-linux-gnu , features: feat_os_unix_gnueabihf } - { os: ubuntu-latest , target: aarch64-unknown-linux-musl , features: feat_os_unix_musl , use-cross: use-cross , skip-tests: true } + - { os: ubuntu-latest , target: riscv64gc-unknown-linux-musl , features: feat_os_unix_musl , use-cross: use-cross , skip-tests: true } # - { os: ubuntu-latest , target: x86_64-unknown-linux-gnu , features: feat_selinux , use-cross: use-cross } - { os: ubuntu-latest , target: i686-unknown-linux-gnu , features: "feat_os_unix,test_risky_names", use-cross: use-cross } - - { os: ubuntu-latest , target: i686-unknown-linux-musl , features: feat_os_unix_musl , use-cross: use-cross } + # glibc 2.42 is important more than this platform + # Wait https://github.com/rust-lang/libc/pull/4914 + #- { os: ubuntu-latest , target: i686-unknown-linux-musl , features: feat_os_unix_musl , use-cross: use-cross } - { os: ubuntu-latest , target: x86_64-unknown-linux-gnu , features: "feat_os_unix,test_risky_names", use-cross: use-cross, skip-publish: true } - { os: ubuntu-latest , target: x86_64-unknown-linux-gnu , features: "feat_os_unix,uudoc" , use-cross: no, workspace-tests: true } - { os: ubuntu-latest , target: x86_64-unknown-linux-musl , features: feat_os_unix_musl , use-cross: use-cross } @@ -636,6 +679,7 @@ jobs: unset TARGET_ARCH case '${{ matrix.job.target }}' in aarch64-*) TARGET_ARCH=arm64 ;; + riscv64gc-*) TARGET_ARCH=riscv64 ;; arm-*-*hf) TARGET_ARCH=armhf ;; i686-*) TARGET_ARCH=i686 ;; x86_64-*) TARGET_ARCH=x86_64 ;; @@ -700,6 +744,7 @@ jobs: STRIP="strip" case ${{ matrix.job.target }} in aarch64-*-linux-*) STRIP="aarch64-linux-gnu-strip" ;; + riscv64gc-*-linux-*) STRIP="riscv64-linux-gnu-strip" ;; arm-*-linux-gnueabihf) STRIP="arm-linux-gnueabihf-strip" ;; *-pc-windows-msvc) STRIP="" ;; esac; @@ -726,6 +771,10 @@ jobs: sudo apt-get -y update sudo apt-get -y install gcc-aarch64-linux-gnu ;; + riscv64gc-unknown-linux-*) + sudo apt-get -y update + sudo apt-get -y install gcc-riscv64-linux-gnu + ;; *-redox*) sudo apt-get -y update sudo apt-get -y install fuse3 libfuse-dev @@ -814,6 +863,8 @@ jobs: if: matrix.job.skip-tests != true shell: bash run: | + command -v sudo && sudo rm -rf /usr/local/lib/android /usr/share/dotnet # avoid no space left + df -h ||: ## Test individual utilities ${{ steps.vars.outputs.CARGO_CMD }} ${{ steps.vars.outputs.CARGO_CMD_OPTIONS }} test --target=${{ matrix.job.target }} \ ${{ matrix.job.cargo-options }} ${{ steps.dep_vars.outputs.CARGO_UTILITY_LIST_OPTIONS }} @@ -1147,6 +1198,16 @@ jobs: flags: ${{ steps.vars.outputs.CODECOV_FLAGS }} name: codecov-umbrella fail_ci_if_error: false + - name: Upload test results to Codecov + if: ${{ !cancelled() }} + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + report_type: test_results + files: target/nextest/coverage/junit.xml + disable_search: true + flags: coverage,${{ matrix.job.os }} + fail_ci_if_error: false test_separately: name: Separate Builds diff --git a/.github/workflows/FixPR.yml b/.github/workflows/FixPR.yml index e8451c5251b..70f42278c35 100644 --- a/.github/workflows/FixPR.yml +++ b/.github/workflows/FixPR.yml @@ -46,7 +46,7 @@ jobs: # Ensure updated '*/Cargo.lock' # * '*/Cargo.lock' is required to be in a format that `cargo` of MinSRV can interpret (eg, v1-format for MinSRV < v1.38) for dir in "." "fuzz"; do - ( cd "$dir" && (cargo fetch --locked --quiet || cargo +${{ steps.vars.outputs.RUST_MIN_SRV }} update) ) + ( cd "$dir" && (cargo fetch --locked --quiet --target $(rustc --print host-tuple) || cargo +${{ steps.vars.outputs.RUST_MIN_SRV }} update) ) done - name: Info shell: bash @@ -65,7 +65,7 @@ jobs: cargo tree -V ## dependencies echo "## dependency list" - cargo fetch --locked --quiet + cargo fetch --locked --quiet --target $(rustc --print host-tuple) ## * using the 'stable' toolchain is necessary to avoid "unexpected '--filter-platform'" errors RUSTUP_TOOLCHAIN=stable cargo tree --locked --no-dedupe -e=no-dev --prefix=none --features ${{ matrix.job.features }} | grep -vE "$PWD" | sort --unique - name: Commit any changes (to '${{ env.BRANCH_TARGET }}') diff --git a/.github/workflows/GnuTests.yml b/.github/workflows/GnuTests.yml index 4d312388b0c..3a7bb002df6 100644 --- a/.github/workflows/GnuTests.yml +++ b/.github/workflows/GnuTests.yml @@ -2,7 +2,7 @@ name: GnuTests # spell-checker:ignore (abbrev/names) CodeCov gnulib GnuTests Swatinem # spell-checker:ignore (jargon) submodules devel -# spell-checker:ignore (libs/utils) autopoint chksum dpkg getenforce getlimits gperf lcov libexpect limactl pyinotify setenforce shopt texinfo valgrind libattr libcap taiki-e zstd cpio +# spell-checker:ignore (libs/utils) chksum dpkg getenforce getlimits gperf lcov libexpect limactl pyinotify setenforce shopt valgrind libattr libcap taiki-e zstd cpio # spell-checker:ignore (options) Ccodegen Coverflow Cpanic Zpanic # spell-checker:ignore (people) Dawid Dziurla * dawidd dtolnay # spell-checker:ignore (vars) FILESET SUBDIRS XPASS @@ -47,7 +47,6 @@ jobs: - uses: dtolnay/rust-toolchain@master with: toolchain: stable - components: rustfmt - uses: Swatinem/rust-cache@v2 with: workspaces: "./uutils -> target" @@ -69,7 +68,7 @@ jobs: ## Install dependencies sudo apt-get update ## Check that build-gnu.sh works on the non SELinux system by installing libselinux only on lima - sudo apt-get install -y autopoint gperf gdb python3-pyinotify valgrind libexpect-perl libacl1-dev libattr1-dev libcap-dev attr quilt + sudo apt-get install -y gperf gdb python3-pyinotify valgrind libexpect-perl libacl1-dev libattr1-dev libcap-dev attr quilt curl http://launchpadlibrarian.net/831710181/automake_1.18.1-3_all.deb > automake-1.18.deb sudo dpkg -i --force-depends automake-1.18.deb - name: Add various locales @@ -105,7 +104,7 @@ jobs: ## Build binaries cd 'uutils' env PROFILE=release-small bash util/build-gnu.sh - + - name: Save files for faster configure and skipping make uses: actions/cache/save@v5 if: always() && steps.cache-config-gnu.outputs.cache-hit != 'true' @@ -211,7 +210,6 @@ jobs: - uses: dtolnay/rust-toolchain@master with: toolchain: stable - components: rustfmt - uses: Swatinem/rust-cache@v2 with: workspaces: "./uutils -> target" @@ -247,8 +245,8 @@ jobs: - name: Install dependencies in VM run: | lima sudo dnf -y update - lima sudo dnf -y install git autoconf autopoint bison texinfo gperf gcc gdb jq libacl-devel libattr-devel libcap-devel libselinux-devel attr rustup clang-devel texinfo-tex automake patch quilt - lima rustup-init -y --default-toolchain stable + lima sudo dnf -y install autoconf bison gperf gcc gdb jq libacl-devel libattr-devel libcap-devel libselinux-devel attr rustup clang-devel automake patch quilt + lima rustup-init -y --profile=minimal --default-toolchain stable - name: Copy the sources to VM run: | rsync -a -e ssh . lima-default:~/work/ @@ -331,7 +329,6 @@ jobs: - uses: dtolnay/rust-toolchain@master with: toolchain: stable - components: rustfmt - uses: Swatinem/rust-cache@v2 with: workspaces: "./uutils -> target" diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 93a9fec1e2f..a4a9b3bd08b 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -166,7 +166,7 @@ jobs: disk-size: ${{ env.EMULATOR_DISK_SIZE }} cores: ${{ env.EMULATOR_CORES }} force-avd-creation: false - emulator-options: ${{ env.COMMON_EMULATOR_OPTIONS }} -no-snapshot-save -snapshot ${{ env.AVD_CACHE_KEY }} + emulator-options: ${{ env.COMMON_EMULATOR_OPTIONS }} -no-metrics -no-snapshot-save -snapshot ${{ env.AVD_CACHE_KEY }} emulator-boot-timeout: ${{ env.EMULATOR_BOOT_TIMEOUT }} # This is not a usual script. Every line is executed in a separate shell with `sh -c`. If # one of the lines returns with error the whole script is failed (like running a script with diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 205f6c1a24e..9f53a016773 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -27,10 +27,11 @@ jobs: - { package: uu_cksum } - { package: uu_cp } - { package: uu_cut } + - { package: uu_dd } - { package: uu_du } - { package: uu_expand } - { package: uu_fold } - - { package: uu_hashsum } + - { package: uu_join } - { package: uu_ls } - { package: uu_mv } - { package: uu_nl } @@ -45,6 +46,7 @@ jobs: - { package: uu_uniq } - { package: uu_wc } - { package: uu_factor } + - { package: uu_date } steps: - uses: actions/checkout@v6 with: @@ -72,7 +74,7 @@ jobs: env: CODSPEED_LOG: debug with: - mode: instrumentation + mode: simulation run: | echo "Running benchmarks for ${{ matrix.benchmark-target.package }}" cargo codspeed run -p ${{ matrix.benchmark-target.package }} > /dev/null diff --git a/.github/workflows/openbsd.yml b/.github/workflows/openbsd.yml index 8bb91566a02..7a14240c828 100644 --- a/.github/workflows/openbsd.yml +++ b/.github/workflows/openbsd.yml @@ -47,7 +47,7 @@ jobs: prepare: | # Clean up disk space before installing packages df -h - rm -rf /usr/share/doc/* /usr/share/man/* /var/cache/* /tmp/* || true + rm -rf /usr/share/relink/* /usr/X11R6/* /usr/share/doc/* /usr/share/man/* || : pkg_add curl sudo-- jq coreutils bash rust rust-clippy rust-rustfmt llvm-- # Clean up package cache after installation pkg_delete -a || true @@ -115,8 +115,6 @@ jobs: fi # Clean to avoid to rsync back the files and free up disk space cargo clean - # Additional cleanup to free disk space - rm -rf ~/.cargo/registry/cache ~/.cargo/git/db || true if [ -n "\${FAIL_ON_FAULT}" ] && [ -n "\${FAULT}" ]; then exit 1 ; fi EOF @@ -144,10 +142,10 @@ jobs: prepare: | # Clean up disk space before installing packages df -h - rm -rf /usr/share/doc/* /usr/share/man/* /var/cache/* /tmp/* || true + rm -rf /usr/share/relink/* /usr/X11R6/* /usr/share/doc/* /usr/share/man/* || : pkg_add curl gmake sudo-- jq rust llvm-- # Clean up package cache after installation - pkg_delete -a || true + pkg_delete -a || : df -h run: | ## Prepare, build, and test @@ -197,8 +195,6 @@ jobs: cd "${WORKSPACE}" unset FAULT cargo build || FAULT=1 - # Clean build artifacts to save disk space before testing - rm -rf target/debug/build target/debug/incremental || true export PATH=~/.cargo/bin:${PATH} export RUST_BACKTRACE=1 export CARGO_TERM_COLOR=always @@ -216,6 +212,5 @@ jobs: # Clean to avoid to rsync back the files and free up disk space cargo clean # Additional cleanup to free disk space - rm -rf ~/.cargo/registry/cache ~/.cargo/git/db target/debug/deps target/release/deps || true if (test -n "\$FAULT"); then exit 1 ; fi EOF diff --git a/.vscode/cspell.dictionaries/jargon.wordlist.txt b/.vscode/cspell.dictionaries/jargon.wordlist.txt index 9fa0b625aac..a1bda0e7690 100644 --- a/.vscode/cspell.dictionaries/jargon.wordlist.txt +++ b/.vscode/cspell.dictionaries/jargon.wordlist.txt @@ -86,6 +86,7 @@ listxattr llistxattr lossily lstat +makedev mebi mebibytes mergeable diff --git a/.vscode/cspell.dictionaries/workspace.wordlist.txt b/.vscode/cspell.dictionaries/workspace.wordlist.txt index 8a8a1474a92..28c468d4f9c 100644 --- a/.vscode/cspell.dictionaries/workspace.wordlist.txt +++ b/.vscode/cspell.dictionaries/workspace.wordlist.txt @@ -182,6 +182,7 @@ LINESIZE NAMESIZE RTLD_NEXT RTLD +SIGABRT SIGINT SIGKILL SIGSTOP @@ -379,6 +380,7 @@ istrip litout opost parodd +ENOTTY # translation tests CLICOLOR diff --git a/Cargo.lock b/Cargo.lock index 277321cd990..c38215bfff7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -345,18 +345,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.53" +version = "4.5.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" +checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.53" +version = "4.5.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" +checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" dependencies = [ "anstream", "anstyle", @@ -367,9 +367,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.62" +version = "4.5.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "004eef6b14ce34759aa7de4aea3217e368f463f46a3ed3764ca4b5a4404003b4" +checksum = "4c0da80818b2d95eca9aa614a30783e42f62bf5fdfee24e68cfb960b071ba8d1" dependencies = [ "clap", ] @@ -392,9 +392,9 @@ dependencies = [ [[package]] name = "codspeed" -version = "4.2.0" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb56923193c76a0e5b6b17b2c2bb1e151ef8a5e06b557e1cbe38c6db467763f9" +checksum = "5f0d98d97fd75ca4489a1a0997820a6521531085e7c8a98941bd0e1264d567dd" dependencies = [ "anyhow", "cc", @@ -410,9 +410,9 @@ dependencies = [ [[package]] name = "codspeed-divan-compat" -version = "4.2.0" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7558ff5740fbc26a5fc55c4934cfed94dfccee76abc17b57ecf5d0bee3592b5e" +checksum = "4179ec5518e79efcd02ed50aa483ff807902e43c85146e87fff58b9cffc06078" dependencies = [ "clap", "codspeed", @@ -423,9 +423,9 @@ dependencies = [ [[package]] name = "codspeed-divan-compat-macros" -version = "4.2.0" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de343ca0a4fbaabbd3422941fdee24407d00e2fa686a96021c21a78ab2bb895" +checksum = "15eaee97aa5bceb32cc683fe25cd6373b7fc48baee5c12471996b58b6ddf0d7c" dependencies = [ "divan-macros", "itertools 0.14.0", @@ -437,9 +437,9 @@ dependencies = [ [[package]] name = "codspeed-divan-compat-walltime" -version = "4.2.0" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d9de586cc7e9752fc232f08e0733c2016122e16065c4adf0c8a8d9e370749ee" +checksum = "c38671153aa73be075d6019cab5ab1e6b31d36644067c1ac4cef73bf9723ce33" dependencies = [ "cfg-if", "clap", @@ -546,6 +546,7 @@ dependencies = [ "fluent-syntax", "glob", "hex-literal", + "jiff", "libc", "nix", "num-prime", @@ -757,7 +758,7 @@ dependencies = [ "mio", "parking_lot", "rustix", - "signal-hook", + "signal-hook 0.3.18", "signal-hook-mio", "winapi", ] @@ -1565,9 +1566,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a87d9b8105c23642f50cbbae03d1f75d8422c5cb98ce7ee9271f7ff7505be6b8" +checksum = "e67e8da4c49d6d9909fe03361f9b620f58898859f5c7aded68351e85e71ecf50" dependencies = [ "jiff-static", "jiff-tzdb-platform", @@ -1575,14 +1576,14 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde_core", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "jiff-static" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b787bebb543f8969132630c51fd0afab173a86c6abae56ff3b9e5e3e3f9f6e58" +checksum = "e0c84ee7f197eca9a86c6fd6cb771e55eb991632f15f2bc3ca6ec838929e6e78" dependencies = [ "proc-macro2", "quote", @@ -1651,9 +1652,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.175" +version = "0.2.179" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f" [[package]] name = "libloading" @@ -1873,7 +1874,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2179,9 +2180,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.104" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0" +checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" dependencies = [ "unicode-ident", ] @@ -2212,9 +2213,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.42" +version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" dependencies = [ "proc-macro2", ] @@ -2439,7 +2440,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.11.0", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2471,9 +2472,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "self_cell" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16c2f82143577edb4921b71ede051dac62ca3c16084e918bf7b40c96ae10eb33" +checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" [[package]] name = "selinux" @@ -2607,6 +2608,16 @@ dependencies = [ "signal-hook-registry", ] +[[package]] +name = "signal-hook" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a37d01603c37b5466f808de79f845c7116049b0579adb70a6b7d47c1fa3a952" +dependencies = [ + "libc", + "signal-hook-registry", +] + [[package]] name = "signal-hook-mio" version = "0.2.5" @@ -2615,7 +2626,7 @@ checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" dependencies = [ "libc", "mio", - "signal-hook", + "signal-hook 0.3.18", ] [[package]] @@ -2701,6 +2712,16 @@ dependencies = [ "num-traits", ] +[[package]] +name = "string-interner" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23de088478b31c349c9ba67816fa55d9355232d63c3afea8bf513e31f0f1d2c0" +dependencies = [ + "hashbrown 0.15.4", + "serde", +] + [[package]] name = "strsim" version = "0.11.1" @@ -2745,7 +2766,7 @@ dependencies = [ "getrandom 0.3.3", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3188,10 +3209,12 @@ name = "uu_date" version = "0.5.0" dependencies = [ "clap", + "codspeed-divan-compat", "fluent", "jiff", "nix", "parse_datetime", + "tempfile", "uucore", "windows-sys 0.61.2", ] @@ -3201,11 +3224,13 @@ name = "uu_dd" version = "0.5.0" dependencies = [ "clap", + "codspeed-divan-compat", "fluent", "gcd", "libc", "nix", - "signal-hook", + "signal-hook 0.4.1", + "tempfile", "thiserror 2.0.17", "uucore", ] @@ -3438,8 +3463,10 @@ name = "uu_join" version = "0.5.0" dependencies = [ "clap", + "codspeed-divan-compat", "fluent", "memchr", + "tempfile", "thiserror 2.0.17", "uucore", ] @@ -4031,6 +4058,8 @@ dependencies = [ "clap", "codspeed-divan-compat", "fluent", + "nix", + "string-interner", "tempfile", "thiserror 2.0.17", "uucore", @@ -4408,7 +4437,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index d6737d16d08..d28e8628dcb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ # coreutils (uutils) # * see the repository LICENSE, README, and CONTRIBUTING files for more information -# spell-checker:ignore (libs) bigdecimal datetime serde bincode gethostid kqueue libselinux mangen memmap uuhelp startswith constness expl unnested logind cfgs +# spell-checker:ignore (libs) bigdecimal datetime serde bincode gethostid kqueue libselinux mangen memmap uuhelp startswith constness expl unnested logind cfgs interner [package] name = "coreutils" @@ -369,7 +369,8 @@ same-file = "1.0.6" self_cell = "1.0.4" # FIXME we use the exact version because the new 0.5.3 requires an MSRV of 1.88 selinux = "=0.5.2" -signal-hook = "0.3.17" +string-interner = "0.19.0" +signal-hook = "0.4.1" tempfile = "3.15.0" terminal_size = "0.4.0" textwrap = { version = "0.16.1", features = ["terminal_size"] } @@ -538,6 +539,7 @@ chrono.workspace = true ctor.workspace = true filetime.workspace = true glob.workspace = true +jiff.workspace = true libc.workspace = true num-prime.workspace = true pretty_assertions = "1.4.0" @@ -618,9 +620,12 @@ workspace = true # This is the linting configuration for all crates. # In order to use these, all crates have `[lints] workspace = true` section. [workspace.lints.rust] -# Allow "fuzzing" as a "cfg" condition name +# Allow "fuzzing" as a "cfg" condition name and "cygwin" as a value for "target_os" # https://doc.rust-lang.org/nightly/rustc/check-cfg/cargo-specifics.html -unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing)'] } +unexpected_cfgs = { level = "warn", check-cfg = [ + 'cfg(fuzzing)', + 'cfg(target_os, values("cygwin"))', +] } #unused_qualifications = "warn" // TODO: fix warnings in uucore, then re-enable this lint [workspace.lints.clippy] @@ -657,7 +662,6 @@ ignored_unit_patterns = "allow" # 21 similar_names = "allow" # 20 large_stack_arrays = "allow" # 20 wildcard_imports = "allow" # 18 -used_underscore_binding = "allow" # 18 needless_pass_by_value = "allow" # 16 float_cmp = "allow" # 12 items_after_statements = "allow" # 11 diff --git a/Cross.toml b/Cross.toml index 52f5bad21dd..90d824e61aa 100644 --- a/Cross.toml +++ b/Cross.toml @@ -5,3 +5,6 @@ pre-build = [ ] [build.env] passthrough = ["CI", "RUST_BACKTRACE", "CARGO_TERM_COLOR"] + +[target.riscv64gc-unknown-linux-musl] +image = "ghcr.io/cross-rs/riscv64gc-unknown-linux-musl:main" diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 4f885e085dd..35291369c12 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -262,6 +262,7 @@ To generate [gcov-based](https://github.com/mozilla/grcov#example-how-to-generat export CARGO_INCREMENTAL=0 export RUSTFLAGS="-Cinstrument-coverage -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort" export RUSTDOCFLAGS="-Cpanic=abort" +export RUSTUP_TOOLCHAIN="nightly" cargo build # e.g., --features feat_os_unix cargo test # e.g., --features feat_os_unix test_pathchk grcov . -s . --binary-path ./target/debug/ -t html --branch --ignore-not-existing --ignore build.rs --excl-br-line "^\s*((debug_)?assert(_eq|_ne)?\#\[derive\()" -o ./target/debug/coverage/ diff --git a/deny.toml b/deny.toml index 662474b65cd..eb0e0230052 100644 --- a/deny.toml +++ b/deny.toml @@ -107,6 +107,8 @@ skip = [ { name = "zerocopy-derive", version = "0.7.35" }, # rustix { name = "linux-raw-sys", version = "0.11.0" }, + # crossterm + { name = "signal-hook", version = "0.3.18" }, ] # spell-checker: enable diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index 2b519a989f3..b891522ccca 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -894,9 +894,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.178" +version = "0.2.179" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f" [[package]] name = "libfuzzer-sys" diff --git a/src/bin/uudoc.rs b/src/bin/uudoc.rs index 5a713e040fa..392375f9edb 100644 --- a/src/bin/uudoc.rs +++ b/src/bin/uudoc.rs @@ -465,7 +465,9 @@ impl MDWriter<'_, '_> { /// # Errors /// Returns an error if the writer fails. fn options(&mut self) -> io::Result<()> { - writeln!(self.w, "

Options

")?; + writeln!(self.w)?; + writeln!(self.w, "## Options")?; + writeln!(self.w)?; write!(self.w, "
")?; for arg in self.command.get_arguments() { write!(self.w, "
")?; @@ -576,7 +578,7 @@ fn format_examples(content: String, output_markdown: bool) -> Result( } // print to end of line or end of buffer - let offset = write_end(&mut writer, &in_buf[pos..], options); + let offset = write_end(&mut writer, &in_buf[pos..], options)?; // end of buffer? if offset + pos == in_buf.len() { @@ -628,7 +628,11 @@ fn write_new_line( Ok(()) } -fn write_end(writer: &mut W, in_buf: &[u8], options: &OutputOptions) -> usize { +fn write_end( + writer: &mut W, + in_buf: &[u8], + options: &OutputOptions, +) -> io::Result { if options.show_nonprint { write_nonprint_to_end(in_buf, writer, options.tab().as_bytes()) } else if options.show_tabs { @@ -644,21 +648,21 @@ fn write_end(writer: &mut W, in_buf: &[u8], options: &OutputOptions) - // however, write_nonprint_to_end doesn't need to stop at \r because it will always write \r as ^M. // Return the number of written symbols -fn write_to_end(in_buf: &[u8], writer: &mut W) -> usize { +fn write_to_end(in_buf: &[u8], writer: &mut W) -> io::Result { // using memchr2 significantly improves performances match memchr2(b'\n', b'\r', in_buf) { Some(p) => { - writer.write_all(&in_buf[..p]).unwrap(); - p + writer.write_all(&in_buf[..p])?; + Ok(p) } None => { - writer.write_all(in_buf).unwrap(); - in_buf.len() + writer.write_all(in_buf)?; + Ok(in_buf.len()) } } } -fn write_tab_to_end(mut in_buf: &[u8], writer: &mut W) -> usize { +fn write_tab_to_end(mut in_buf: &[u8], writer: &mut W) -> io::Result { let mut count = 0; loop { match in_buf @@ -666,25 +670,25 @@ fn write_tab_to_end(mut in_buf: &[u8], writer: &mut W) -> usize { .position(|c| *c == b'\n' || *c == b'\t' || *c == b'\r') { Some(p) => { - writer.write_all(&in_buf[..p]).unwrap(); + writer.write_all(&in_buf[..p])?; if in_buf[p] == b'\t' { - writer.write_all(b"^I").unwrap(); + writer.write_all(b"^I")?; in_buf = &in_buf[p + 1..]; count += p + 1; } else { // b'\n' or b'\r' - return count + p; + return Ok(count + p); } } None => { - writer.write_all(in_buf).unwrap(); - return in_buf.len() + count; + writer.write_all(in_buf)?; + return Ok(in_buf.len() + count); } } } } -fn write_nonprint_to_end(in_buf: &[u8], writer: &mut W, tab: &[u8]) -> usize { +fn write_nonprint_to_end(in_buf: &[u8], writer: &mut W, tab: &[u8]) -> io::Result { let mut count = 0; for byte in in_buf.iter().copied() { @@ -699,11 +703,10 @@ fn write_nonprint_to_end(in_buf: &[u8], writer: &mut W, tab: &[u8]) -> 128..=159 => writer.write_all(&[b'M', b'-', b'^', byte - 64]), 160..=254 => writer.write_all(&[b'M', b'-', byte - 128]), _ => writer.write_all(b"M-^?"), - } - .unwrap(); + }?; count += 1; } - count + Ok(count) } fn write_end_of_line( @@ -733,14 +736,14 @@ mod tests { fn test_write_tab_to_end_with_newline() { let mut writer = BufWriter::with_capacity(1024 * 64, stdout()); let in_buf = b"a\tb\tc\n"; - assert_eq!(super::write_tab_to_end(in_buf, &mut writer), 5); + assert_eq!(super::write_tab_to_end(in_buf, &mut writer).unwrap(), 5); } #[test] fn test_write_tab_to_end_no_newline() { let mut writer = BufWriter::with_capacity(1024 * 64, stdout()); let in_buf = b"a\tb\tc"; - assert_eq!(super::write_tab_to_end(in_buf, &mut writer), 5); + assert_eq!(super::write_tab_to_end(in_buf, &mut writer).unwrap(), 5); } #[test] @@ -748,7 +751,7 @@ mod tests { let mut writer = BufWriter::with_capacity(1024 * 64, stdout()); let in_buf = b"\n"; let tab = b""; - super::write_nonprint_to_end(in_buf, &mut writer, tab); + super::write_nonprint_to_end(in_buf, &mut writer, tab).unwrap(); assert_eq!(writer.buffer().len(), 0); } @@ -757,7 +760,7 @@ mod tests { let mut writer = BufWriter::with_capacity(1024 * 64, stdout()); let in_buf = &[9u8]; let tab = b"tab"; - super::write_nonprint_to_end(in_buf, &mut writer, tab); + super::write_nonprint_to_end(in_buf, &mut writer, tab).unwrap(); assert_eq!(writer.buffer(), tab); } @@ -767,7 +770,7 @@ mod tests { let mut writer = BufWriter::with_capacity(1024 * 64, stdout()); let in_buf = &[byte]; let tab = b""; - super::write_nonprint_to_end(in_buf, &mut writer, tab); + super::write_nonprint_to_end(in_buf, &mut writer, tab).unwrap(); assert_eq!(writer.buffer(), [b'^', byte + 64]); } } @@ -778,7 +781,7 @@ mod tests { let mut writer = BufWriter::with_capacity(1024 * 64, stdout()); let in_buf = &[byte]; let tab = b""; - super::write_nonprint_to_end(in_buf, &mut writer, tab); + super::write_nonprint_to_end(in_buf, &mut writer, tab).unwrap(); assert_eq!(writer.buffer(), [b'^', byte + 64]); } } diff --git a/src/uu/chmod/src/chmod.rs b/src/uu/chmod/src/chmod.rs index 24566272b23..43760b45017 100644 --- a/src/uu/chmod/src/chmod.rs +++ b/src/uu/chmod/src/chmod.rs @@ -407,11 +407,11 @@ impl Chmoder { // should not change the permissions in this case continue; } - if self.recursive && self.preserve_root && file == Path::new("/") { + if self.recursive && self.preserve_root && Self::is_root(file) { return Err(ChmodError::PreserveRoot("/".into()).into()); } if self.recursive { - r = self.walk_dir_with_context(file, true); + r = self.walk_dir_with_context(file, true).and(r); } else { r = self.chmod_file(file).and(r); } @@ -419,6 +419,10 @@ impl Chmoder { r } + fn is_root(file: impl AsRef) -> bool { + matches!(fs::canonicalize(&file), Ok(p) if p == Path::new("/")) + } + #[cfg(not(target_os = "linux"))] fn walk_dir_with_context(&self, file_path: &Path, is_command_line_arg: bool) -> UResult<()> { let mut r = self.chmod_file(file_path); @@ -432,14 +436,20 @@ impl Chmoder { // If the path is a directory (or we should follow symlinks), recurse into it if (!file_path.is_symlink() || should_follow_symlink) && file_path.is_dir() { + // We buffer all paths in this dir to not keep to be able to close the fd so not + // too many fd's are open during the recursion + let mut paths_in_this_dir = Vec::new(); + for dir_entry in file_path.read_dir()? { - let path = match dir_entry { - Ok(entry) => entry.path(), + match dir_entry { + Ok(entry) => paths_in_this_dir.push(entry.path()), Err(err) => { r = r.and(Err(err.into())); continue; } - }; + } + } + for path in paths_in_this_dir { if path.is_symlink() { r = self.handle_symlink_during_recursion(&path).and(r); } else { diff --git a/src/uu/cksum/src/cksum.rs b/src/uu/cksum/src/cksum.rs index 30eabcaac56..72c7984f049 100644 --- a/src/uu/cksum/src/cksum.rs +++ b/src/uu/cksum/src/cksum.rs @@ -7,8 +7,7 @@ use clap::builder::ValueParser; use clap::{Arg, ArgAction, Command}; -use std::ffi::{OsStr, OsString}; -use std::iter; +use std::ffi::OsString; use uucore::checksum::compute::{ ChecksumComputeOptions, figure_out_output_format, perform_checksum_computation, }; @@ -74,42 +73,6 @@ mod options { /// Returns a pair of boolean. The first one indicates if we should use tagged /// output format, the second one indicates if we should use the binary flag in /// the untagged case. -fn handle_tag_text_binary_flags>( - args: impl Iterator, -) -> UResult<(bool, bool)> { - let mut tag = true; - let mut binary = false; - let mut text = false; - - // --binary, --tag and --untagged are tight together: none of them - // conflicts with each other but --tag will reset "binary" and "text" and - // set "tag". - - for arg in args { - let arg = arg.as_ref(); - if arg == "-b" || arg == "--binary" { - text = false; - binary = true; - } else if arg == "--text" { - text = true; - binary = false; - } else if arg == "--tag" { - tag = true; - binary = false; - text = false; - } else if arg == "--untagged" { - tag = false; - } - } - - // Specifying --text without ever mentioning --untagged fails. - if text && tag { - return Err(ChecksumError::TextWithoutUntagged.into()); - } - - Ok((tag, binary)) -} - /// Sanitize the `--length` argument depending on `--algorithm` and `--length`. fn maybe_sanitize_length( algo_cli: Option, @@ -140,19 +103,11 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let check = matches.get_flag(options::CHECK); - let check_flag = |flag| match (check, matches.get_flag(flag)) { - (_, false) => Ok(false), - (true, true) => Ok(true), - (false, true) => Err(ChecksumError::CheckOnlyFlag(flag.into())), - }; - - // Each of the following flags are only expected in --check mode. - // If we encounter them otherwise, end with an error. - let ignore_missing = check_flag(options::IGNORE_MISSING)?; - let warn = check_flag(options::WARN)?; - let quiet = check_flag(options::QUIET)?; - let strict = check_flag(options::STRICT)?; - let status = check_flag(options::STATUS)?; + let ignore_missing = matches.get_flag(options::IGNORE_MISSING); + let warn = matches.get_flag(options::WARN); + let quiet = matches.get_flag(options::QUIET); + let strict = matches.get_flag(options::STRICT); + let status = matches.get_flag(options::STATUS); let algo_cli = matches .get_one::(options::ALGORITHM) @@ -165,12 +120,11 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let length = maybe_sanitize_length(algo_cli, input_length)?; - let files = matches.get_many::(options::FILE).map_or_else( - // No files given, read from stdin. - || Box::new(iter::once(OsStr::new("-"))) as Box>, - // At least one file given, read from them. - |files| Box::new(files.map(OsStr::new)) as Box>, - ); + // clap provides the default value -. So we unwrap() safety. + let files = matches + .get_many::(options::FILE) + .unwrap() + .map(|s| s.as_os_str()); if check { // cksum does not support '--check'ing legacy algorithms @@ -178,14 +132,6 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { return Err(ChecksumError::AlgorithmNotSupportedWithCheck.into()); } - let text_flag = matches.get_flag(options::TEXT); - let binary_flag = matches.get_flag(options::BINARY); - let tag = matches.get_flag(options::TAG); - - if tag || binary_flag || text_flag { - return Err(ChecksumError::BinaryTextConflict.into()); - } - // Execute the checksum validation based on the presence of files or the use of stdin let verbose = ChecksumVerbose::new(status, quiet, warn); @@ -208,7 +154,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // Set the default algorithm to CRC when not '--check'ing. let algo_kind = algo_cli.unwrap_or(AlgoKind::Crc); - let (tag, binary) = handle_tag_text_binary_flags(std::env::args_os())?; + let tag = !matches.get_flag(options::UNTAGGED); // Making TAG default at clap blocks --untagged + let binary = matches.get_flag(options::BINARY); let algo = SizedAlgoKind::from_unsized(algo_kind, length)?; let line_ending = LineEnding::from_zero_flag(matches.get_flag(options::ZERO)); @@ -243,6 +190,8 @@ pub fn uu_app() -> Command { .hide(true) .action(ArgAction::Append) .value_parser(ValueParser::os_string()) + .default_value("-") + .hide_default_value(true) .value_hint(clap::ValueHint::FilePath), ) .arg( @@ -265,7 +214,9 @@ pub fn uu_app() -> Command { .long(options::TAG) .help(translate!("cksum-help-tag")) .action(ArgAction::SetTrue) - .overrides_with(options::UNTAGGED), + .overrides_with(options::UNTAGGED) + .overrides_with(options::BINARY) + .overrides_with(options::TEXT), ) .arg( Arg::new(options::LENGTH) @@ -284,13 +235,17 @@ pub fn uu_app() -> Command { Arg::new(options::STRICT) .long(options::STRICT) .help(translate!("cksum-help-strict")) - .action(ArgAction::SetTrue), + .action(ArgAction::SetTrue) + .requires(options::CHECK), ) .arg( Arg::new(options::CHECK) .short('c') .long(options::CHECK) .help(translate!("cksum-help-check")) + .conflicts_with(options::TAG) + .conflicts_with(options::BINARY) + .conflicts_with(options::TEXT) .action(ArgAction::SetTrue), ) .arg( @@ -308,7 +263,8 @@ pub fn uu_app() -> Command { .short('t') .hide(true) .overrides_with(options::BINARY) - .action(ArgAction::SetTrue), + .action(ArgAction::SetTrue) + .requires(options::UNTAGGED), ) .arg( Arg::new(options::BINARY) @@ -324,27 +280,31 @@ pub fn uu_app() -> Command { .long("warn") .help(translate!("cksum-help-warn")) .action(ArgAction::SetTrue) - .overrides_with_all([options::STATUS, options::QUIET]), + .overrides_with_all([options::STATUS, options::QUIET]) + .requires(options::CHECK), ) .arg( Arg::new(options::STATUS) .long("status") .help(translate!("cksum-help-status")) .action(ArgAction::SetTrue) - .overrides_with_all([options::WARN, options::QUIET]), + .overrides_with_all([options::WARN, options::QUIET]) + .requires(options::CHECK), ) .arg( Arg::new(options::QUIET) .long(options::QUIET) .help(translate!("cksum-help-quiet")) .action(ArgAction::SetTrue) - .overrides_with_all([options::WARN, options::STATUS]), + .overrides_with_all([options::WARN, options::STATUS]) + .requires(options::CHECK), ) .arg( Arg::new(options::IGNORE_MISSING) .long(options::IGNORE_MISSING) .help(translate!("cksum-help-ignore-missing")) - .action(ArgAction::SetTrue), + .action(ArgAction::SetTrue) + .requires(options::CHECK), ) .arg( Arg::new(options::ZERO) diff --git a/src/uu/cp/benches/cp_bench.rs b/src/uu/cp/benches/cp_bench.rs index ba29596d93c..d673c14e48c 100644 --- a/src/uu/cp/benches/cp_bench.rs +++ b/src/uu/cp/benches/cp_bench.rs @@ -4,24 +4,11 @@ // file that was distributed with this source code. use divan::{Bencher, black_box}; -use std::fs::{self, File}; -use std::io::Write; +use std::fs; use std::path::Path; use tempfile::TempDir; use uu_cp::uumain; -use uucore::benchmark::{fs_tree, run_util_function}; - -fn remove_path(path: &Path) { - if !path.exists() { - return; - } - - if path.is_dir() { - fs::remove_dir_all(path).unwrap(); - } else { - fs::remove_file(path).unwrap(); - } -} +use uucore::benchmark::{binary_data, fs_tree, fs_utils, run_util_function}; fn bench_cp_directory(bencher: Bencher, args: &[&str], setup_source: F) where @@ -38,7 +25,7 @@ where let dest_str = dest.to_str().unwrap(); bencher.bench(|| { - remove_path(&dest); + fs_utils::remove_path(&dest); let mut full_args = Vec::with_capacity(args.len() + 2); full_args.extend_from_slice(args); @@ -99,16 +86,13 @@ fn cp_large_file(bencher: Bencher, size_mb: usize) { let source = temp_dir.path().join("source.bin"); let dest = temp_dir.path().join("dest.bin"); - let buffer = vec![b'x'; size_mb * 1024 * 1024]; - let mut file = File::create(&source).unwrap(); - file.write_all(&buffer).unwrap(); - file.sync_all().unwrap(); + binary_data::create_file(&source, size_mb, b'x'); let source_str = source.to_str().unwrap(); let dest_str = dest.to_str().unwrap(); bencher.bench(|| { - remove_path(&dest); + fs_utils::remove_path(&dest); black_box(run_util_function(uumain, &[source_str, dest_str])); }); diff --git a/src/uu/cp/src/copydir.rs b/src/uu/cp/src/copydir.rs index bbd3aba6297..6ac1ae09072 100644 --- a/src/uu/cp/src/copydir.rs +++ b/src/uu/cp/src/copydir.rs @@ -22,7 +22,6 @@ use uucore::fs::{ FileInformation, MissingHandling, ResolveMode, canonicalize, path_ends_with_terminator, }; use uucore::show; -use uucore::show_error; use uucore::translate; use uucore::uio_error; use walkdir::{DirEntry, WalkDir}; @@ -513,7 +512,7 @@ pub(crate) fn copy_directory( } // Print an error message, but continue traversing the directory. - Err(e) => show_error!("{e}"), + Err(e) => show!(CpError::WalkDirErr(e)), } } diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index 036e9f9ee55..cd84caa36af 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -689,7 +689,12 @@ pub fn uu_app() -> Command { Arg::new(options::NO_DEREFERENCE) .short('P') .long(options::NO_DEREFERENCE) - .overrides_with(options::DEREFERENCE) + .overrides_with_all([ + options::DEREFERENCE, + options::CLI_SYMBOLIC_LINKS, + options::ARCHIVE, + options::NO_DEREFERENCE_PRESERVE_LINKS, + ]) // -d sets this option .help(translate!("cp-help-no-dereference")) .action(ArgAction::SetTrue), @@ -698,13 +703,24 @@ pub fn uu_app() -> Command { Arg::new(options::DEREFERENCE) .short('L') .long(options::DEREFERENCE) - .overrides_with(options::NO_DEREFERENCE) + .overrides_with_all([ + options::NO_DEREFERENCE, + options::CLI_SYMBOLIC_LINKS, + options::ARCHIVE, + options::NO_DEREFERENCE_PRESERVE_LINKS, + ]) .help(translate!("cp-help-dereference")) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::CLI_SYMBOLIC_LINKS) .short('H') + .overrides_with_all([ + options::DEREFERENCE, + options::NO_DEREFERENCE, + options::ARCHIVE, + options::NO_DEREFERENCE_PRESERVE_LINKS, + ]) .help(translate!("cp-help-cli-symbolic-links")) .action(ArgAction::SetTrue), ) @@ -712,12 +728,24 @@ pub fn uu_app() -> Command { Arg::new(options::ARCHIVE) .short('a') .long(options::ARCHIVE) + .overrides_with_all([ + options::DEREFERENCE, + options::NO_DEREFERENCE, + options::CLI_SYMBOLIC_LINKS, + options::NO_DEREFERENCE_PRESERVE_LINKS, + ]) .help(translate!("cp-help-archive")) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::NO_DEREFERENCE_PRESERVE_LINKS) .short('d') + .overrides_with_all([ + options::DEREFERENCE, + options::NO_DEREFERENCE, + options::CLI_SYMBOLIC_LINKS, + options::ARCHIVE, + ]) .help(translate!("cp-help-no-dereference-preserve-links")) .action(ArgAction::SetTrue), ) @@ -1279,9 +1307,7 @@ fn parse_path_args( }; if options.strip_trailing_slashes { - // clippy::assigning_clones added with Rust 1.78 - // Rust version = 1.76 on OpenBSD stable/7.5 - #[cfg_attr(not(target_os = "openbsd"), allow(clippy::assigning_clones))] + #[allow(clippy::assigning_clones)] for source in &mut paths { *source = source.components().as_path().to_owned(); } @@ -1364,8 +1390,8 @@ pub fn copy(sources: &[PathBuf], target: &Path, options: &Options) -> CopyResult let dest = construct_dest_path(source, target, target_type, options) .unwrap_or_else(|_| target.to_path_buf()); - if fs::metadata(&dest).is_ok() - && !fs::symlink_metadata(&dest)?.file_type().is_symlink() + if FileInformation::from_path(&dest, true).is_ok() + && !fs::symlink_metadata(&dest).is_ok_and(|m| m.file_type().is_symlink()) // if both `source` and `dest` are symlinks, it should be considered as an overwrite. || fs::metadata(source).is_ok() && fs::symlink_metadata(source)?.file_type().is_symlink() @@ -2510,11 +2536,13 @@ fn copy_file( } if options.dereference(source_in_command_line) { - if let Ok(src) = canonicalize(source, MissingHandling::Normal, ResolveMode::Physical) { - if src.exists() { - copy_attributes(&src, dest, &options.attributes)?; - } - } + // Try to canonicalize, but if it fails (e.g., due to inaccessible parent directories), + // fall back to the original source path + let src_for_attrs = canonicalize(source, MissingHandling::Normal, ResolveMode::Physical) + .ok() + .filter(|p| p.exists()) + .unwrap_or_else(|| source.to_path_buf()); + copy_attributes(&src_for_attrs, dest, &options.attributes)?; } else if source_is_stream && !source.exists() { // Some stream files may not exist after we have copied it, // like anonymous pipes. Thus, we can't really copy its diff --git a/src/uu/csplit/src/csplit.rs b/src/uu/csplit/src/csplit.rs index 01cc4e0dc47..1c2978cea0f 100644 --- a/src/uu/csplit/src/csplit.rs +++ b/src/uu/csplit/src/csplit.rs @@ -127,7 +127,7 @@ where let ret = do_csplit(&mut split_writer, patterns_vec, &mut input_iter); // consume the rest, unless there was an error - if ret.is_ok() { + let ret = if ret.is_ok() { input_iter.rewind_buffer(); if let Some((_, line)) = input_iter.next() { // There is remaining input: create a final split and copy remainder @@ -136,14 +136,18 @@ where for (_, line) in input_iter { split_writer.writeln(&line?)?; } - split_writer.finish_split(); + split_writer.finish_split() } else if all_up_to_line && options.suppress_matched { // GNU semantics for integer patterns with --suppress-matched: // even if no remaining input, create a final (possibly empty) split split_writer.new_writer()?; - split_writer.finish_split(); + split_writer.finish_split() + } else { + Ok(()) } - } + } else { + ret + }; // delete files on error by default if ret.is_err() && !options.keep_files { split_writer.delete_all_splits()?; @@ -305,15 +309,24 @@ impl SplitWriter<'_> { /// /// # Errors /// - /// Some [`io::Error`] if the split could not be removed in case it should be elided. - fn finish_split(&mut self) { + /// Returns an error if flushing the writer fails. + fn finish_split(&mut self) -> Result<(), CsplitError> { if !self.dev_null { + // Flush the writer to ensure all data is written and errors are detected + if let Some(ref mut writer) = self.current_writer { + let file_name = self.options.split_name.get(self.counter - 1); + writer + .flush() + .map_err_context(|| file_name.clone()) + .map_err(CsplitError::from)?; + } if self.options.elide_empty_files && self.size == 0 { self.counter -= 1; } else if !self.options.quiet { println!("{}", self.size); } } + Ok(()) } /// Removes all the split files that were created. @@ -379,7 +392,7 @@ impl SplitWriter<'_> { } self.writeln(&line)?; } - self.finish_split(); + self.finish_split()?; ret } @@ -446,7 +459,7 @@ impl SplitWriter<'_> { self.writeln(&line?)?; } None => { - self.finish_split(); + self.finish_split()?; return Err(CsplitError::LineOutOfRange( pattern_as_str.to_string(), )); @@ -454,7 +467,7 @@ impl SplitWriter<'_> { } offset -= 1; } - self.finish_split(); + self.finish_split()?; // if we have to suppress one line after we take the next and do nothing if next_line_suppress_matched { @@ -495,7 +508,7 @@ impl SplitWriter<'_> { ); } - self.finish_split(); + self.finish_split()?; if input_iter.buffer_len() < offset_usize { return Err(CsplitError::LineOutOfRange(pattern_as_str.to_string())); } @@ -511,7 +524,7 @@ impl SplitWriter<'_> { } } - self.finish_split(); + self.finish_split()?; Err(CsplitError::MatchNotFound(pattern_as_str.to_string())) } } diff --git a/src/uu/date/Cargo.toml b/src/uu/date/Cargo.toml index 431868b9175..9bff97696f0 100644 --- a/src/uu/date/Cargo.toml +++ b/src/uu/date/Cargo.toml @@ -41,3 +41,12 @@ windows-sys = { workspace = true, features = [ [[bin]] name = "date" path = "src/main.rs" + +[dev-dependencies] +divan = { workspace = true } +tempfile = { workspace = true } +uucore = { workspace = true, features = ["benchmark"] } + +[[bench]] +name = "date_bench" +harness = false diff --git a/src/uu/date/benches/date_bench.rs b/src/uu/date/benches/date_bench.rs new file mode 100644 index 00000000000..1c1d05aaea2 --- /dev/null +++ b/src/uu/date/benches/date_bench.rs @@ -0,0 +1,79 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use divan::{Bencher, black_box}; +use std::io::Write; +use tempfile::NamedTempFile; +use uu_date::uumain; +use uucore::benchmark::run_util_function; + +/// Helper to create a temporary file containing N lines of date strings. +fn setup_date_file(lines: usize, date_format: &str) -> NamedTempFile { + let mut file = NamedTempFile::new().unwrap(); + for _ in 0..lines { + writeln!(file, "{date_format}").unwrap(); + } + file +} + +/// Benchmarks processing a file containing simple ISO dates. +#[divan::bench] +fn file_iso_dates(bencher: Bencher) { + let count = 1_000; + let file = setup_date_file(count, "2023-05-10 12:00:00"); + let path = file.path().to_str().unwrap(); + + bencher.bench(|| { + black_box(run_util_function(uumain, &["-f", path])); + }); +} + +/// Benchmarks processing a file containing dates with Timezone abbreviations. +#[divan::bench] +fn file_tz_abbreviations(bencher: Bencher) { + let count = 1_000; + // "EST" triggers the abbreviation lookup and double-parsing logic + let file = setup_date_file(count, "2023-05-10 12:00:00 EST"); + let path = file.path().to_str().unwrap(); + + bencher.bench(|| { + black_box(run_util_function(uumain, &["-f", path])); + }); +} + +/// Benchmarks formatting speed using a custom output format. +#[divan::bench] +fn file_custom_format(bencher: Bencher) { + let count = 1_000; + let file = setup_date_file(count, "2023-05-10 12:00:00"); + let path = file.path().to_str().unwrap(); + + bencher.bench(|| { + black_box(run_util_function(uumain, &["-f", path, "+%A %d %B %Y"])); + }); +} + +/// Benchmarks the overhead of starting the utility for a single date (no file). +#[divan::bench] +fn single_date_now(bencher: Bencher) { + bencher.bench(|| { + black_box(run_util_function(uumain, &[])); + }); +} + +/// Benchmarks parsing a complex relative date string passed as an argument. +#[divan::bench] +fn complex_relative_date(bencher: Bencher) { + bencher.bench(|| { + black_box(run_util_function( + uumain, + &["--date=last friday 12:00 + 2 days"], + )); + }); +} + +fn main() { + divan::main(); +} diff --git a/src/uu/date/locales/en-US.ftl b/src/uu/date/locales/en-US.ftl index 80f82649da2..782275fec6e 100644 --- a/src/uu/date/locales/en-US.ftl +++ b/src/uu/date/locales/en-US.ftl @@ -105,3 +105,4 @@ date-error-setting-date-not-supported-macos = setting the date is not supported date-error-setting-date-not-supported-redox = setting the date is not supported by Redox date-error-cannot-set-date = cannot set date date-error-extra-operand = extra operand '{$operand}' +date-error-write = write error: {$error} diff --git a/src/uu/date/locales/fr-FR.ftl b/src/uu/date/locales/fr-FR.ftl index 1967c958a2b..15321c1fcdc 100644 --- a/src/uu/date/locales/fr-FR.ftl +++ b/src/uu/date/locales/fr-FR.ftl @@ -100,3 +100,4 @@ date-error-setting-date-not-supported-macos = la définition de la date n'est pa date-error-setting-date-not-supported-redox = la définition de la date n'est pas prise en charge par Redox date-error-cannot-set-date = impossible de définir la date date-error-extra-operand = opérande supplémentaire '{$operand}' +date-error-write = erreur d'écriture: {$error} diff --git a/src/uu/date/src/date.rs b/src/uu/date/src/date.rs index c72b1c3048b..5baa75432b6 100644 --- a/src/uu/date/src/date.rs +++ b/src/uu/date/src/date.rs @@ -13,7 +13,7 @@ use jiff::tz::{TimeZone, TimeZoneDatabase}; use jiff::{Timestamp, Zoned}; use std::collections::HashMap; use std::fs::File; -use std::io::{BufRead, BufReader}; +use std::io::{BufRead, BufReader, BufWriter, Write}; use std::path::PathBuf; use std::sync::OnceLock; use uucore::display::Quotable; @@ -116,6 +116,20 @@ impl From<&str> for Rfc3339Format { } } +/// Indicates whether parsing a military timezone causes the date to remain the same, roll back to the previous day, or +/// advance to the next day. +/// This can occur when applying a military timezone with an optional hour offset crosses midnight +/// in either direction. +#[derive(PartialEq, Debug)] +enum DayDelta { + /// The date does not change + Same, + /// The date rolls back to the previous day. + Previous, + /// The date advances to the next day. + Next, +} + /// Parse military timezone with optional hour offset. /// Pattern: single letter (a-z except j) optionally followed by 1-2 digits. /// Returns Some(total_hours_in_utc) or None if pattern doesn't match. @@ -128,7 +142,7 @@ impl From<&str> for Rfc3339Format { /// /// The hour offset from digits is added to the base military timezone offset. /// Examples: "m" -> 12 (noon UTC), "m9" -> 21 (9pm UTC), "a5" -> 4 (4am UTC next day) -fn parse_military_timezone_with_offset(s: &str) -> Option { +fn parse_military_timezone_with_offset(s: &str) -> Option<(i32, DayDelta)> { if s.is_empty() || s.len() > 3 { return None; } @@ -160,11 +174,17 @@ fn parse_military_timezone_with_offset(s: &str) -> Option { _ => return None, }; + let day_delta = match additional_hours - tz_offset { + h if h < 0 => DayDelta::Previous, + h if h >= 24 => DayDelta::Next, + _ => DayDelta::Same, + }; + // Calculate total hours: midnight (0) + tz_offset + additional_hours // Midnight in timezone X converted to UTC - let total_hours = (0 - tz_offset + additional_hours).rem_euclid(24); + let hours_from_midnight = (0 - tz_offset + additional_hours).rem_euclid(24); - Some(total_hours) + Some((hours_from_midnight, day_delta)) } #[uucore::main] @@ -306,11 +326,24 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { format!("{date_part} 00:00 {offset}") }; parse_date(composed) - } else if let Some(total_hours) = military_tz_with_offset { + } else if let Some((total_hours, day_delta)) = military_tz_with_offset { // Military timezone with optional hour offset // Convert to UTC time: midnight + military_tz_offset + additional_hours - let date_part = - strtime::format("%F", &now).unwrap_or_else(|_| String::from("1970-01-01")); + + // When calculating a military timezone with an optional hour offset, midnight may + // be crossed in either direction. `day_delta` indicates whether the date remains + // the same, moves to the previous day, or advances to the next day. + // Changing day can result in error, this closure will help handle these errors + // gracefully. + let format_date_with_epoch_fallback = |date: Result| -> String { + date.and_then(|d| strtime::format("%F", &d)) + .unwrap_or_else(|_| String::from("1970-01-01")) + }; + let date_part = match day_delta { + DayDelta::Same => format_date_with_epoch_fallback(Ok(now)), + DayDelta::Next => format_date_with_epoch_fallback(now.tomorrow()), + DayDelta::Previous => format_date_with_epoch_fallback(now.yesterday()), + }; let composed = format!("{date_part} {total_hours:02}:00:00 +00:00"); parse_date(composed) } else if is_pure_digits { @@ -395,24 +428,31 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { }; let format_string = make_format_string(&settings); + let mut stdout = BufWriter::new(std::io::stdout().lock()); // Format all the dates for date in dates { match date { // TODO: Switch to lenient formatting. Ok(date) => match strtime::format(format_string, &date) { - Ok(s) => println!("{s}"), + Ok(s) => writeln!(stdout, "{s}").map_err(|e| { + USimpleError::new(1, translate!("date-error-write", "error" => e)) + })?, Err(e) => { + let _ = stdout.flush(); return Err(USimpleError::new( 1, translate!("date-error-invalid-format", "format" => format_string, "error" => e), )); } }, - Err((input, _err)) => show!(USimpleError::new( - 1, - translate!("date-error-invalid-date", "date" => input) - )), + Err((input, _err)) => { + let _ = stdout.flush(); + show!(USimpleError::new( + 1, + translate!("date-error-invalid-date", "date" => input) + )); + } } } @@ -638,9 +678,12 @@ fn tz_abbrev_to_iana(abbrev: &str) -> Option<&str> { cache.get(abbrev).map(|s| s.as_str()) } -/// Resolve timezone abbreviation in date string and replace with numeric offset. -/// Returns the modified string with offset, or original if no abbreviation found. -fn resolve_tz_abbreviation>(date_str: S) -> String { +/// Attempts to parse a date string that contains a timezone abbreviation (e.g. "EST"). +/// +/// If an abbreviation is found and the date is parsable, returns `Some(Zoned)`. +/// Returns `None` if no abbreviation is detected or if parsing fails, indicating +/// that standard parsing should be attempted. +fn try_parse_with_abbreviation>(date_str: S) -> Option { let s = date_str.as_ref(); // Look for timezone abbreviation at the end of the string @@ -664,11 +707,7 @@ fn resolve_tz_abbreviation>(date_str: S) -> String { let ts = parsed.timestamp(); // Get the offset for this specific timestamp in the target timezone - let zoned = ts.to_zoned(tz); - let offset_str = format!("{}", zoned.offset()); - - // Replace abbreviation with offset - return format!("{date_part} {offset_str}"); + return Some(ts.to_zoned(tz)); } } } @@ -676,7 +715,7 @@ fn resolve_tz_abbreviation>(date_str: S) -> String { } // No abbreviation found or couldn't resolve, return original - s.to_string() + None } /// Parse a `String` into a `DateTime`. @@ -691,10 +730,12 @@ fn resolve_tz_abbreviation>(date_str: S) -> String { fn parse_date + Clone>( s: S, ) -> Result { - // First, try to resolve any timezone abbreviations - let resolved = resolve_tz_abbreviation(s.as_ref()); + // First, try to parse any timezone abbreviations + if let Some(zoned) = try_parse_with_abbreviation(s.as_ref()) { + return Ok(zoned); + } - match parse_datetime::parse_datetime(&resolved) { + match parse_datetime::parse_datetime(s.as_ref()) { Ok(date) => { // Convert to system timezone for display // (parse_datetime 0.13 returns Zoned in the input's timezone) @@ -817,11 +858,26 @@ mod tests { #[test] fn test_parse_military_timezone_with_offset() { // Valid cases: letter only, letter + digit, uppercase - assert_eq!(parse_military_timezone_with_offset("m"), Some(12)); // UTC+12 -> 12:00 UTC - assert_eq!(parse_military_timezone_with_offset("m9"), Some(21)); // 12 + 9 = 21 - assert_eq!(parse_military_timezone_with_offset("a5"), Some(4)); // 23 + 5 = 28 % 24 = 4 - assert_eq!(parse_military_timezone_with_offset("z"), Some(0)); // UTC+0 -> 00:00 UTC - assert_eq!(parse_military_timezone_with_offset("M9"), Some(21)); // Uppercase works + assert_eq!( + parse_military_timezone_with_offset("m"), + Some((12, DayDelta::Previous)) + ); // UTC+12 -> 12:00 UTC + assert_eq!( + parse_military_timezone_with_offset("m9"), + Some((21, DayDelta::Previous)) + ); // 12 + 9 = 21 + assert_eq!( + parse_military_timezone_with_offset("a5"), + Some((4, DayDelta::Same)) + ); // 23 + 5 = 28 % 24 = 4 + assert_eq!( + parse_military_timezone_with_offset("z"), + Some((0, DayDelta::Same)) + ); // UTC+0 -> 00:00 UTC + assert_eq!( + parse_military_timezone_with_offset("M9"), + Some((21, DayDelta::Previous)) + ); // Uppercase works // Invalid cases: 'j' reserved, empty, too long, starts with digit assert_eq!(parse_military_timezone_with_offset("j"), None); // Reserved for local time diff --git a/src/uu/dd/Cargo.toml b/src/uu/dd/Cargo.toml index d1ac79fb52e..6dbc6c2ffa0 100644 --- a/src/uu/dd/Cargo.toml +++ b/src/uu/dd/Cargo.toml @@ -37,3 +37,12 @@ nix = { workspace = true, features = ["fs"] } [[bin]] name = "dd" path = "src/main.rs" + +[dev-dependencies] +divan = { workspace = true } +tempfile = { workspace = true } +uucore = { workspace = true, features = ["benchmark"] } + +[[bench]] +name = "dd_bench" +harness = false diff --git a/src/uu/dd/benches/dd_bench.rs b/src/uu/dd/benches/dd_bench.rs new file mode 100644 index 00000000000..b08207e7ecc --- /dev/null +++ b/src/uu/dd/benches/dd_bench.rs @@ -0,0 +1,259 @@ +// 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 tempfile::TempDir; +use uu_dd::uumain; +use uucore::benchmark::{binary_data, fs_utils, run_util_function}; + +/// Benchmark basic dd copy with default settings +#[divan::bench] +fn dd_copy_default(bencher: Bencher) { + let size_mb = 32; + let temp_dir = TempDir::new().unwrap(); + let input = temp_dir.path().join("input.bin"); + let output = temp_dir.path().join("output.bin"); + + binary_data::create_file(&input, size_mb, b'x'); + + let input_str = input.to_str().unwrap(); + let output_str = output.to_str().unwrap(); + + bencher.bench(|| { + fs_utils::remove_path(&output); + black_box(run_util_function( + uumain, + &[ + &format!("if={input_str}"), + &format!("of={output_str}"), + "status=none", + ], + )); + }); +} + +/// Benchmark dd copy with 4KB block size (common page size) +#[divan::bench] +fn dd_copy_4k_blocks(bencher: Bencher) { + let size_mb = 24; + let temp_dir = TempDir::new().unwrap(); + let input = temp_dir.path().join("input.bin"); + let output = temp_dir.path().join("output.bin"); + + binary_data::create_file(&input, size_mb, b'x'); + + let input_str = input.to_str().unwrap(); + let output_str = output.to_str().unwrap(); + + bencher.bench(|| { + fs_utils::remove_path(&output); + black_box(run_util_function( + uumain, + &[ + &format!("if={input_str}"), + &format!("of={output_str}"), + "bs=4K", + "status=none", + ], + )); + }); +} + +/// Benchmark dd copy with 64KB block size +#[divan::bench] +fn dd_copy_64k_blocks(bencher: Bencher) { + let size_mb = 64; + let temp_dir = TempDir::new().unwrap(); + let input = temp_dir.path().join("input.bin"); + let output = temp_dir.path().join("output.bin"); + + binary_data::create_file(&input, size_mb, b'x'); + + let input_str = input.to_str().unwrap(); + let output_str = output.to_str().unwrap(); + + bencher.bench(|| { + fs_utils::remove_path(&output); + black_box(run_util_function( + uumain, + &[ + &format!("if={input_str}"), + &format!("of={output_str}"), + "bs=64K", + "status=none", + ], + )); + }); +} + +/// Benchmark dd copy with 1MB block size +#[divan::bench] +fn dd_copy_1m_blocks(bencher: Bencher) { + let size_mb = 128; + let temp_dir = TempDir::new().unwrap(); + let input = temp_dir.path().join("input.bin"); + let output = temp_dir.path().join("output.bin"); + + binary_data::create_file(&input, size_mb, b'x'); + + let input_str = input.to_str().unwrap(); + let output_str = output.to_str().unwrap(); + + bencher.bench(|| { + fs_utils::remove_path(&output); + black_box(run_util_function( + uumain, + &[ + &format!("if={input_str}"), + &format!("of={output_str}"), + "bs=1M", + "status=none", + ], + )); + }); +} + +/// Benchmark dd copy with separate input and output block sizes +#[divan::bench] +fn dd_copy_separate_blocks(bencher: Bencher) { + let size_mb = 48; + let temp_dir = TempDir::new().unwrap(); + let input = temp_dir.path().join("input.bin"); + let output = temp_dir.path().join("output.bin"); + + binary_data::create_file(&input, size_mb, b'x'); + + let input_str = input.to_str().unwrap(); + let output_str = output.to_str().unwrap(); + + bencher.bench(|| { + fs_utils::remove_path(&output); + black_box(run_util_function( + uumain, + &[ + &format!("if={input_str}"), + &format!("of={output_str}"), + "ibs=8K", + "obs=16K", + "status=none", + ], + )); + }); +} + +/// Benchmark dd with count limit (partial copy) +#[divan::bench] +fn dd_copy_partial(bencher: Bencher) { + let size_mb = 32; + let temp_dir = TempDir::new().unwrap(); + let input = temp_dir.path().join("input.bin"); + let output = temp_dir.path().join("output.bin"); + + binary_data::create_file(&input, size_mb, b'x'); + + let input_str = input.to_str().unwrap(); + let output_str = output.to_str().unwrap(); + + bencher.bench(|| { + fs_utils::remove_path(&output); + black_box(run_util_function( + uumain, + &[ + &format!("if={input_str}"), + &format!("of={output_str}"), + "bs=4K", + "count=1024", + "status=none", + ], + )); + }); +} + +/// Benchmark dd with skip (seeking in input) +#[divan::bench] +fn dd_copy_with_skip(bencher: Bencher) { + let size_mb = 48; + let temp_dir = TempDir::new().unwrap(); + let input = temp_dir.path().join("input.bin"); + let output = temp_dir.path().join("output.bin"); + + binary_data::create_file(&input, size_mb, b'x'); + + let input_str = input.to_str().unwrap(); + let output_str = output.to_str().unwrap(); + + bencher.bench(|| { + fs_utils::remove_path(&output); + black_box(run_util_function( + uumain, + &[ + &format!("if={input_str}"), + &format!("of={output_str}"), + "bs=4K", + "skip=256", + "status=none", + ], + )); + }); +} + +/// Benchmark dd with seek (seeking in output) +#[divan::bench] +fn dd_copy_with_seek(bencher: Bencher) { + let size_mb = 48; + let temp_dir = TempDir::new().unwrap(); + let input = temp_dir.path().join("input.bin"); + let output = temp_dir.path().join("output.bin"); + + binary_data::create_file(&input, size_mb, b'x'); + + let input_str = input.to_str().unwrap(); + let output_str = output.to_str().unwrap(); + + bencher.bench(|| { + fs_utils::remove_path(&output); + black_box(run_util_function( + uumain, + &[ + &format!("if={input_str}"), + &format!("of={output_str}"), + "bs=4K", + "seek=256", + "status=none", + ], + )); + }); +} + +/// Benchmark dd with different block sizes for comparison +#[divan::bench] +fn dd_copy_8k_blocks(bencher: Bencher) { + let size_mb = 32; + let temp_dir = TempDir::new().unwrap(); + let input = temp_dir.path().join("input.bin"); + let output = temp_dir.path().join("output.bin"); + + binary_data::create_file(&input, size_mb, b'x'); + + let input_str = input.to_str().unwrap(); + let output_str = output.to_str().unwrap(); + + bencher.bench(|| { + fs_utils::remove_path(&output); + black_box(run_util_function( + uumain, + &[ + &format!("if={input_str}"), + &format!("of={output_str}"), + "bs=8K", + "status=none", + ], + )); + }); +} + +fn main() { + divan::main(); +} diff --git a/src/uu/dd/src/dd.rs b/src/uu/dd/src/dd.rs index 412b6668fe9..567f803d390 100644 --- a/src/uu/dd/src/dd.rs +++ b/src/uu/dd/src/dd.rs @@ -219,17 +219,6 @@ impl Source { Self::StdinFile(f) } - /// The length of the data source in number of bytes. - /// - /// If it cannot be determined, then this function returns 0. - fn len(&self) -> io::Result { - #[allow(clippy::match_wildcard_for_single_variants)] - match self { - Self::File(f) => Ok(f.metadata()?.len().try_into().unwrap_or(i64::MAX)), - _ => Ok(0), - } - } - fn skip(&mut self, n: u64) -> io::Result { match self { #[cfg(not(unix))] @@ -673,17 +662,6 @@ impl Dest { _ => Err(Errno::ESPIPE), // "Illegal seek" } } - - /// The length of the data destination in number of bytes. - /// - /// If it cannot be determined, then this function returns 0. - fn len(&self) -> io::Result { - #[allow(clippy::match_wildcard_for_single_variants)] - match self { - Self::File(f, _) => Ok(f.metadata()?.len().try_into().unwrap_or(i64::MAX)), - _ => Ok(0), - } - } } /// Decide whether the given buffer is all zeros. @@ -1063,21 +1041,12 @@ impl BlockWriter<'_> { /// depending on the command line arguments, this function /// informs the OS to flush/discard the caches for input and/or output file. fn flush_caches_full_length(i: &Input, o: &Output) -> io::Result<()> { - // TODO Better error handling for overflowing `len`. + // Using len=0 in posix_fadvise means "to end of file" if i.settings.iflags.nocache { - let offset = 0; - #[allow(clippy::useless_conversion)] - let len = i.src.len()?.try_into().unwrap(); - i.discard_cache(offset, len); + i.discard_cache(0, 0); } - // Similarly, discard the system cache for the output file. - // - // TODO Better error handling for overflowing `len`. if i.settings.oflags.nocache { - let offset = 0; - #[allow(clippy::useless_conversion)] - let len = o.dst.len()?.try_into().unwrap(); - o.discard_cache(offset, len); + o.discard_cache(0, 0); } Ok(()) @@ -1185,6 +1154,7 @@ fn dd_copy(mut i: Input, o: Output) -> io::Result<()> { let input_nocache = i.settings.iflags.nocache; let output_nocache = o.settings.oflags.nocache; + let output_direct = o.settings.oflags.direct; // Add partial block buffering, if needed. let mut o = if o.settings.buffered { @@ -1208,6 +1178,12 @@ fn dd_copy(mut i: Input, o: Output) -> io::Result<()> { let loop_bsize = calc_loop_bsize(i.settings.count, &rstat, &wstat, i.settings.ibs, bsize); let rstat_update = read_helper(&mut i, &mut buf, loop_bsize)?; if rstat_update.is_empty() { + if input_nocache { + i.discard_cache(read_offset.try_into().unwrap(), 0); + } + if output_nocache || output_direct { + o.discard_cache(write_offset.try_into().unwrap(), 0); + } break; } let wstat_update = o.write_blocks(&buf)?; diff --git a/src/uu/df/src/filesystem.rs b/src/uu/df/src/filesystem.rs index 25743941db0..7d9e7cf5ef0 100644 --- a/src/uu/df/src/filesystem.rs +++ b/src/uu/df/src/filesystem.rs @@ -121,7 +121,7 @@ where impl Filesystem { // TODO: resolve uuid in `mount_info.dev_name` if exists pub(crate) fn new(mount_info: MountInfo, file: Option) -> Option { - let _stat_path = if mount_info.mount_dir.is_empty() { + let stat_path = if mount_info.mount_dir.is_empty() { #[cfg(unix)] { mount_info.dev_name.clone().into() @@ -135,9 +135,9 @@ impl Filesystem { mount_info.mount_dir.clone() }; #[cfg(unix)] - let usage = FsUsage::new(statfs(&_stat_path).ok()?); + let usage = FsUsage::new(statfs(&stat_path).ok()?); #[cfg(windows)] - let usage = FsUsage::new(Path::new(&_stat_path)).ok()?; + let usage = FsUsage::new(Path::new(&stat_path)).ok()?; Some(Self { file, mount_info, @@ -291,9 +291,7 @@ mod tests { } #[test] - // clippy::assigning_clones added with Rust 1.78 - // Rust version = 1.76 on OpenBSD stable/7.5 - #[cfg_attr(not(target_os = "openbsd"), allow(clippy::assigning_clones))] + #[allow(clippy::assigning_clones)] fn test_dev_name_match() { let tmp = tempfile::TempDir::new().expect("Failed to create temp dir"); let dev_name = std::fs::canonicalize(tmp.path()) diff --git a/src/uu/du/src/du.rs b/src/uu/du/src/du.rs index 5fd824d61db..1b8084e2ebb 100644 --- a/src/uu/du/src/du.rs +++ b/src/uu/du/src/du.rs @@ -501,10 +501,7 @@ fn safe_du( // Handle inodes if let Some(inode) = this_stat.inode { - if seen_inodes.contains(&inode) && (!options.count_links || !options.all) { - if options.count_links && !options.all { - my_stat.inodes += 1; - } + if seen_inodes.contains(&inode) && !options.count_links { continue; } seen_inodes.insert(inode); @@ -660,13 +657,7 @@ fn du_regular( if let Some(inode) = this_stat.inode { // Check if the inode has been seen before and if we should skip it - if seen_inodes.contains(&inode) - && (!options.count_links || !options.all) - { - // If `count_links` is enabled and `all` is not, increment the inode count - if options.count_links && !options.all { - my_stat.inodes += 1; - } + if seen_inodes.contains(&inode) && !options.count_links { // Skip further processing for this inode continue; } diff --git a/src/uu/env/src/env.rs b/src/uu/env/src/env.rs index e71581f8617..d715d3e9ec1 100644 --- a/src/uu/env/src/env.rs +++ b/src/uu/env/src/env.rs @@ -749,27 +749,25 @@ impl EnvAppData { do_debug_printing: bool, ) -> Result<(), Box> { let prog = Cow::from(opts.program[0]); - #[cfg(unix)] - let mut arg0 = prog.clone(); - #[cfg(not(unix))] - let arg0 = prog.clone(); - let args = &opts.program[1..]; - if let Some(_argv0) = opts.argv0 { - #[cfg(unix)] - { - arg0 = Cow::Borrowed(_argv0); + let arg0 = match opts.argv0 { + None => prog.clone(), + Some(argv0) if cfg!(unix) => { + let arg0 = Cow::Borrowed(argv0); if do_debug_printing { eprintln!("argv0: {}", arg0.quote()); } + arg0 + } + Some(_) => { + return Err(USimpleError::new( + 2, + translate!("env-error-argv0-not-supported"), + )); } + }; - #[cfg(not(unix))] - return Err(USimpleError::new( - 2, - translate!("env-error-argv0-not-supported"), - )); - } + let args = &opts.program[1..]; if do_debug_printing { eprintln!("executing: {}", prog.maybe_quote()); diff --git a/src/uu/hashsum/BENCHMARKING.md b/src/uu/hashsum/BENCHMARKING.md deleted file mode 100644 index 9508cae1b66..00000000000 --- a/src/uu/hashsum/BENCHMARKING.md +++ /dev/null @@ -1,11 +0,0 @@ -# Benchmarking hashsum - -## To bench blake2 - -Taken from: - -With a large file: - -```shell -hyperfine "./target/release/coreutils hashsum --b2sum large-file" "b2sum large-file" -``` diff --git a/src/uu/hashsum/Cargo.toml b/src/uu/hashsum/Cargo.toml index ec382870bdc..f77c2c52d84 100644 --- a/src/uu/hashsum/Cargo.toml +++ b/src/uu/hashsum/Cargo.toml @@ -30,7 +30,3 @@ path = "src/main.rs" divan = { workspace = true } tempfile = { workspace = true } uucore = { workspace = true, features = ["benchmark"] } - -[[bench]] -name = "hashsum_bench" -harness = false diff --git a/src/uu/hashsum/benches/hashsum_bench.rs b/src/uu/hashsum/benches/hashsum_bench.rs deleted file mode 100644 index 27572c560b4..00000000000 --- a/src/uu/hashsum/benches/hashsum_bench.rs +++ /dev/null @@ -1,138 +0,0 @@ -// This file is part of the uutils coreutils package. -// -// For the full copyright and license information, please view the LICENSE -// file that was distributed with this source code. - -use divan::{Bencher, black_box}; -use std::io::Write; -use tempfile::NamedTempFile; -use uu_hashsum::uumain; -use uucore::benchmark::{run_util_function, setup_test_file, text_data}; - -/// Benchmark MD5 hashing -#[divan::bench] -fn hashsum_md5(bencher: Bencher) { - let data = text_data::generate_by_size(10, 80); - let file_path = setup_test_file(&data); - - bencher.bench(|| { - black_box(run_util_function( - uumain, - &["--md5", file_path.to_str().unwrap()], - )); - }); -} - -/// Benchmark SHA1 hashing -#[divan::bench] -fn hashsum_sha1(bencher: Bencher) { - let data = text_data::generate_by_size(10, 80); - let file_path = setup_test_file(&data); - - bencher.bench(|| { - black_box(run_util_function( - uumain, - &["--sha1", file_path.to_str().unwrap()], - )); - }); -} - -/// Benchmark SHA256 hashing -#[divan::bench] -fn hashsum_sha256(bencher: Bencher) { - let data = text_data::generate_by_size(10, 80); - let file_path = setup_test_file(&data); - - bencher.bench(|| { - black_box(run_util_function( - uumain, - &["--sha256", file_path.to_str().unwrap()], - )); - }); -} - -/// Benchmark SHA512 hashing -#[divan::bench] -fn hashsum_sha512(bencher: Bencher) { - let data = text_data::generate_by_size(10, 80); - let file_path = setup_test_file(&data); - - bencher.bench(|| { - black_box(run_util_function( - uumain, - &["--sha512", file_path.to_str().unwrap()], - )); - }); -} - -/// Benchmark MD5 checksum verification -#[divan::bench] -fn hashsum_md5_check(bencher: Bencher) { - bencher - .with_inputs(|| { - // Create test file - let data = text_data::generate_by_size(10, 80); - let test_file = setup_test_file(&data); - - // Create checksum file - keep it alive by returning it - let checksum_file = NamedTempFile::new().unwrap(); - let checksum_path = checksum_file.path().to_str().unwrap().to_string(); - - // Write checksum content - { - let mut file = std::fs::File::create(&checksum_path).unwrap(); - writeln!( - file, - "d41d8cd98f00b204e9800998ecf8427e {}", - test_file.to_str().unwrap() - ) - .unwrap(); - } - - (checksum_file, checksum_path) - }) - .bench_values(|(_checksum_file, checksum_path)| { - black_box(run_util_function( - uumain, - &["--md5", "--check", &checksum_path], - )); - }); -} - -/// Benchmark SHA256 checksum verification -#[divan::bench] -fn hashsum_sha256_check(bencher: Bencher) { - bencher - .with_inputs(|| { - // Create test file - let data = text_data::generate_by_size(10, 80); - let test_file = setup_test_file(&data); - - // Create checksum file - keep it alive by returning it - let checksum_file = NamedTempFile::new().unwrap(); - let checksum_path = checksum_file.path().to_str().unwrap().to_string(); - - // Write checksum content - { - let mut file = std::fs::File::create(&checksum_path).unwrap(); - writeln!( - file, - "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 {}", - test_file.to_str().unwrap() - ) - .unwrap(); - } - - (checksum_file, checksum_path) - }) - .bench_values(|(_checksum_file, checksum_path)| { - black_box(run_util_function( - uumain, - &["--sha256", "--check", &checksum_path], - )); - }); -} - -fn main() { - divan::main(); -} diff --git a/src/uu/hashsum/src/hashsum.rs b/src/uu/hashsum/src/hashsum.rs index 19e8ad9db04..a13ec468436 100644 --- a/src/uu/hashsum/src/hashsum.rs +++ b/src/uu/hashsum/src/hashsum.rs @@ -157,40 +157,20 @@ pub fn uumain(mut args: impl uucore::Args) -> UResult<()> { }; let check = matches.get_flag("check"); - let check_flag = |flag| match (check, matches.get_flag(flag)) { - (_, false) => Ok(false), - (true, true) => Ok(true), - (false, true) => Err(ChecksumError::CheckOnlyFlag(flag.into())), - }; - - // Each of the following flags are only expected in --check mode. - // If we encounter them otherwise, end with an error. - let ignore_missing = check_flag("ignore-missing")?; - let warn = check_flag("warn")?; - let quiet = check_flag("quiet")?; - let strict = check_flag("strict")?; - let status = check_flag("status")?; - - let 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>, - ); + let ignore_missing = matches.get_flag("ignore-missing"); + let warn = matches.get_flag("warn"); + let quiet = matches.get_flag("quiet"); + let strict = matches.get_flag("strict"); + let status = matches.get_flag("status"); + + // clap provides the default value -. So we unwrap() safety. + let files = matches + .get_many::(options::FILE) + .unwrap() + .map(|s| s.as_os_str()); if check { - // on Windows, allow --binary/--text to be used with --check - // and keep the behavior of defaulting to binary - #[cfg(not(windows))] - { - let text_flag = matches.get_flag("text"); - let binary_flag = matches.get_flag("binary"); - - if binary_flag || text_flag { - return Err(ChecksumError::BinaryTextConflict.into()); - } - } - + // No reason to allow --check with --binary/--text on Cygwin. It want to be same with Linux and --text was broken for a long time. let verbose = ChecksumVerbose::new(status, quiet, warn); let opts = ChecksumValidateOptions { @@ -240,6 +220,10 @@ mod options { } pub fn uu_app_common() -> Command { + // --text --arg-deps-check should be error by Arg::new(options::CHECK)...conflicts_with(options::TEXT) + // https://github.com/clap-rs/clap/issues/4520 ? + // Let --{warn,strict,quiet,status,ignore-missing} reject --text and remove them later. + // Bad error message, but not a lie... Command::new(uucore::util_name()) .version(uucore::crate_version!()) .help_template(uucore::localized_help_template(uucore::util_name())) @@ -269,7 +253,9 @@ pub fn uu_app_common() -> Command { .long("check") .help(translate!("hashsum-help-check")) .action(ArgAction::SetTrue) - .conflicts_with("tag"), + .conflicts_with(options::BINARY) + .conflicts_with(options::TEXT) + .conflicts_with(options::TAG), ) .arg( Arg::new(options::TAG) @@ -301,7 +287,9 @@ pub fn uu_app_common() -> Command { .long(options::QUIET) .help(translate!("hashsum-help-quiet")) .action(ArgAction::SetTrue) - .overrides_with_all([options::STATUS, options::WARN]), + .overrides_with_all([options::STATUS, options::WARN]) + .conflicts_with("text") + .requires(options::CHECK), ) .arg( Arg::new(options::STATUS) @@ -309,19 +297,25 @@ pub fn uu_app_common() -> Command { .long("status") .help(translate!("hashsum-help-status")) .action(ArgAction::SetTrue) - .overrides_with_all([options::QUIET, options::WARN]), + .overrides_with_all([options::QUIET, options::WARN]) + .conflicts_with("text") + .requires(options::CHECK), ) .arg( Arg::new(options::STRICT) .long("strict") .help(translate!("hashsum-help-strict")) - .action(ArgAction::SetTrue), + .action(ArgAction::SetTrue) + .conflicts_with("text") + .requires(options::CHECK), ) .arg( Arg::new("ignore-missing") .long("ignore-missing") .help(translate!("hashsum-help-ignore-missing")) - .action(ArgAction::SetTrue), + .action(ArgAction::SetTrue) + .conflicts_with("text") + .requires(options::CHECK), ) .arg( Arg::new(options::WARN) @@ -329,7 +323,9 @@ pub fn uu_app_common() -> Command { .long("warn") .help(translate!("hashsum-help-warn")) .action(ArgAction::SetTrue) - .overrides_with_all([options::QUIET, options::STATUS]), + .overrides_with_all([options::QUIET, options::STATUS]) + .conflicts_with("text") + .requires(options::CHECK), ) .arg( Arg::new("zero") @@ -343,6 +339,8 @@ pub fn uu_app_common() -> Command { .index(1) .action(ArgAction::Append) .value_name(options::FILE) + .default_value("-") + .hide_default_value(true) .value_hint(clap::ValueHint::FilePath) .value_parser(ValueParser::os_string()), ) diff --git a/src/uu/install/locales/en-US.ftl b/src/uu/install/locales/en-US.ftl index 0261f7320a2..9020db818da 100644 --- a/src/uu/install/locales/en-US.ftl +++ b/src/uu/install/locales/en-US.ftl @@ -48,7 +48,8 @@ install-error-mutually-exclusive-compare-preserve = Options --compare and --pres install-error-mutually-exclusive-compare-strip = Options --compare and --strip are mutually exclusive install-error-missing-file-operand = missing file operand install-error-missing-destination-operand = missing destination file operand after { $path } -install-error-failed-to-remove = Failed to remove existing file { $path }. Error: { $error } +install-error-failed-to-remove = failed to remove existing file { $path }: { $error } +install-error-cannot-stat = cannot stat { $path }: { $error } # Warning messages install-warning-compare-ignored = the --compare (-C) option is ignored when you specify a mode with non-permission bits diff --git a/src/uu/install/locales/fr-FR.ftl b/src/uu/install/locales/fr-FR.ftl index 208712c2187..8296698dd46 100644 --- a/src/uu/install/locales/fr-FR.ftl +++ b/src/uu/install/locales/fr-FR.ftl @@ -48,7 +48,8 @@ install-error-mutually-exclusive-compare-preserve = Les options --compare et --p install-error-mutually-exclusive-compare-strip = Les options --compare et --strip sont mutuellement exclusives install-error-missing-file-operand = opérande de fichier manquant install-error-missing-destination-operand = opérande de fichier de destination manquant après { $path } -install-error-failed-to-remove = Échec de la suppression du fichier existant { $path }. Erreur : { $error } +install-error-failed-to-remove = Échec de la suppression du fichier existant { $path }: { $error } +install-error-cannot-stat = impossible d'obtenir des informations sur le fichier. { $path }: { $error } # Messages d'avertissement install-warning-compare-ignored = l'option --compare (-C) est ignorée quand un mode est indiqué avec des bits non liés à des droits diff --git a/src/uu/install/src/install.rs b/src/uu/install/src/install.rs index 8e43d1fd2ea..4dc03ae92a6 100644 --- a/src/uu/install/src/install.rs +++ b/src/uu/install/src/install.rs @@ -23,6 +23,7 @@ use uucore::backup_control::{self, BackupMode}; use uucore::buf_copy::copy_stream; use uucore::display::Quotable; use uucore::entries::{grp2gid, usr2uid}; +use uucore::error::UIoError; use uucore::error::{FromIo, UError, UResult, UUsageError}; use uucore::fs::dir_strip_dot_for_creation; use uucore::perms::{Verbosity, VerbosityLevel, wrap_chown}; @@ -118,6 +119,12 @@ enum InstallError { #[error("{}", translate!("install-error-extra-operand", "operand" => .0.quote(), "usage" => .1.clone()))] ExtraOperand(OsString, String), + #[error("{}", translate!("install-error-failed-to-remove", "path" => .0.quote(), "error" => .1.clone()))] + FailedToRemove(PathBuf, String), + + #[error("{}", translate!("install-error-cannot-stat", "path" => .0.quote(), "error" => .1.clone()))] + CannotStat(PathBuf, String), + #[cfg(feature = "selinux")] #[error("{}", .0)] SelinuxContextFailed(String), @@ -675,7 +682,19 @@ fn standard(mut paths: Vec, b: &Behavior) -> UResult<()> { return copy_files_into_dir(sources, &target, b); } - if target.is_file() || is_new_file_path(&target) { + // target.is_file does not detect special files like character/block devices + // So, in a unix environment, we need to check the file type from metadata and + // not just trust is_file(). + #[cfg(unix)] + let is_file = match metadata(&target) { + Ok(meta) => !meta.file_type().is_dir(), + Err(_) => false, + }; + + #[cfg(not(unix))] + let is_file = target.is_file(); + + if is_file || is_new_file_path(&target) { copy(source, &target, b) } else { Err(InstallError::InvalidTarget(target).into()) @@ -831,6 +850,15 @@ fn copy_file(from: &Path, to: &Path) -> UResult<()> { } } + // If we don't include this check, then the `remove_file` below will fail + // and it will give an incorrect error message. However, if we check to + // see if the file exists, and it can't even be checked, this means we + // don't have permission to access the file, so we should return an error. + if let Err(to_stat) = to.try_exists() { + let err = UIoError::from(to_stat); + return Err(InstallError::CannotStat(to.to_path_buf(), err.to_string()).into()); + } + if to.is_dir() && !from.is_dir() { return Err(InstallError::OverrideDirectoryFailed( to.to_path_buf().clone(), @@ -842,10 +870,11 @@ fn copy_file(from: &Path, to: &Path) -> UResult<()> { // so lets just remove all existing files at destination before copy. if let Err(e) = fs::remove_file(to) { if e.kind() != std::io::ErrorKind::NotFound { - show_error!( - "{}", - translate!("install-error-failed-to-remove", "path" => to.quote(), "error" => format!("{e:?}")) - ); + // If we get here, then remove_file failed for some + // reason other than the file not existing. This means + // this should be a fatal error, not a warning. + let err = UIoError::from(e); + return Err(InstallError::FailedToRemove(to.to_path_buf(), err.to_string()).into()); } } diff --git a/src/uu/join/Cargo.toml b/src/uu/join/Cargo.toml index cc93d5e18b1..401cb3bb57c 100644 --- a/src/uu/join/Cargo.toml +++ b/src/uu/join/Cargo.toml @@ -27,3 +27,12 @@ fluent = { workspace = true } [[bin]] name = "join" path = "src/main.rs" + +[dev-dependencies] +divan = { workspace = true } +tempfile = { workspace = true } +uucore = { workspace = true, features = ["benchmark"] } + +[[bench]] +name = "join_bench" +harness = false diff --git a/src/uu/join/benches/join_bench.rs b/src/uu/join/benches/join_bench.rs new file mode 100644 index 00000000000..798f4344fb0 --- /dev/null +++ b/src/uu/join/benches/join_bench.rs @@ -0,0 +1,131 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use divan::{Bencher, black_box}; +use std::{fs::File, io::Write}; +use tempfile::TempDir; +use uu_join::uumain; +use uucore::benchmark::run_util_function; + +/// Create two sorted files with matching keys for join benchmarking +fn create_join_files(temp_dir: &TempDir, num_lines: usize) -> (String, String) { + let file1_path = temp_dir.path().join("file1.txt"); + let file2_path = temp_dir.path().join("file2.txt"); + + let mut file1 = File::create(&file1_path).unwrap(); + let mut file2 = File::create(&file2_path).unwrap(); + + for i in 0..num_lines { + writeln!(file1, "{i:08} field1_{i} field2_{i}").unwrap(); + writeln!(file2, "{i:08} data1_{i} data2_{i}").unwrap(); + } + + ( + file1_path.to_str().unwrap().to_string(), + file2_path.to_str().unwrap().to_string(), + ) +} + +/// Create two files with partial overlap for join benchmarking +fn create_partial_overlap_files( + temp_dir: &TempDir, + num_lines: usize, + overlap_ratio: f64, +) -> (String, String) { + let file1_path = temp_dir.path().join("file1.txt"); + let file2_path = temp_dir.path().join("file2.txt"); + + let mut file1 = File::create(&file1_path).unwrap(); + let mut file2 = File::create(&file2_path).unwrap(); + + let overlap_count = (num_lines as f64 * overlap_ratio) as usize; + + // File 1: keys 0 to num_lines-1 + for i in 0..num_lines { + writeln!(file1, "{i:08} f1_data_{i}").unwrap(); + } + + // File 2: keys (num_lines - overlap_count) to (2*num_lines - overlap_count - 1) + let start = num_lines - overlap_count; + for i in 0..num_lines { + writeln!(file2, "{:08} f2_data_{}", start + i, i).unwrap(); + } + + ( + file1_path.to_str().unwrap().to_string(), + file2_path.to_str().unwrap().to_string(), + ) +} + +/// Benchmark basic join with fully matching keys +#[divan::bench] +fn join_full_match(bencher: Bencher) { + let num_lines = 10000; + let temp_dir = TempDir::new().unwrap(); + let (file1, file2) = create_join_files(&temp_dir, num_lines); + + bencher.bench(|| { + black_box(run_util_function(uumain, &[&file1, &file2])); + }); +} + +/// Benchmark join with partial overlap (50%) +#[divan::bench] +fn join_partial_overlap(bencher: Bencher) { + let num_lines = 10000; + let temp_dir = TempDir::new().unwrap(); + let (file1, file2) = create_partial_overlap_files(&temp_dir, num_lines, 0.5); + + bencher.bench(|| { + black_box(run_util_function(uumain, &[&file1, &file2])); + }); +} + +/// Benchmark join with custom field separator +#[divan::bench] +fn join_custom_separator(bencher: Bencher) { + let num_lines = 10000; + let temp_dir = TempDir::new().unwrap(); + let file1_path = temp_dir.path().join("file1.txt"); + let file2_path = temp_dir.path().join("file2.txt"); + + let mut file1 = File::create(&file1_path).unwrap(); + let mut file2 = File::create(&file2_path).unwrap(); + + for i in 0..num_lines { + writeln!(file1, "{i:08}\tfield1_{i}\tfield2_{i}").unwrap(); + writeln!(file2, "{i:08}\tdata1_{i}\tdata2_{i}").unwrap(); + } + + let file1_str = file1_path.to_str().unwrap(); + let file2_str = file2_path.to_str().unwrap(); + + bencher.bench(|| { + black_box(run_util_function( + uumain, + &["-t", "\t", file1_str, file2_str], + )); + }); +} + +/// Benchmark join with French locale (fr_FR.UTF-8) +#[divan::bench] +fn join_french_locale(bencher: Bencher) { + let num_lines = 10000; + let temp_dir = TempDir::new().unwrap(); + let (file1, file2) = create_join_files(&temp_dir, num_lines); + + bencher + .with_inputs(|| unsafe { + std::env::set_var("LC_ALL", "fr_FR.UTF-8"); + }) + .bench_values(|_| { + black_box(run_util_function(uumain, &[&file1, &file2])); + }); +} + +fn main() { + divan::main(); +} diff --git a/src/uu/mknod/Cargo.toml b/src/uu/mknod/Cargo.toml index 50e7e2fce3c..32b983134e3 100644 --- a/src/uu/mknod/Cargo.toml +++ b/src/uu/mknod/Cargo.toml @@ -21,7 +21,7 @@ path = "src/mknod.rs" [dependencies] clap = { workspace = true } libc = { workspace = true } -uucore = { workspace = true, features = ["mode"] } +uucore = { workspace = true, features = ["mode", "fs"] } fluent = { workspace = true } [features] diff --git a/src/uu/mknod/src/mknod.rs b/src/uu/mknod/src/mknod.rs index cc22aee5f5c..8a4cf82d01e 100644 --- a/src/uu/mknod/src/mknod.rs +++ b/src/uu/mknod/src/mknod.rs @@ -13,6 +13,7 @@ use std::ffi::CString; use uucore::display::Quotable; use uucore::error::{UResult, USimpleError, UUsageError, set_exit_code}; use uucore::format_usage; +use uucore::fs::makedev; use uucore::translate; const MODE_RW_UGO: mode_t = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH; @@ -26,12 +27,6 @@ mod options { pub const CONTEXT: &str = "context"; } -#[inline(always)] -fn makedev(maj: u64, min: u64) -> dev_t { - // pick up from - ((min & 0xff) | ((maj & 0xfff) << 8) | ((min & !0xff) << 12) | ((maj & !0xfff) << 32)) as dev_t -} - #[derive(Clone, PartialEq)] enum FileType { Block, @@ -145,7 +140,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { translate!("mknod-error-fifo-no-major-minor"), )); } - (_, Some(&major), Some(&minor)) => makedev(major, minor), + (_, Some(&major), Some(&minor)) => makedev(major as _, minor as _), _ => { return Err(UUsageError::new( 1, diff --git a/src/uu/mv/src/mv.rs b/src/uu/mv/src/mv.rs index a43b92eb850..860683e7a6e 100644 --- a/src/uu/mv/src/mv.rs +++ b/src/uu/mv/src/mv.rs @@ -1027,7 +1027,7 @@ fn copy_dir_contents( } #[cfg(not(unix))] { - copy_dir_contents_recursive(from, to, None, None, verbose, progress_bar, display_manager)?; + copy_dir_contents_recursive(from, to, verbose, progress_bar, display_manager)?; } Ok(()) @@ -1038,8 +1038,6 @@ fn copy_dir_contents_recursive( to_dir: &Path, #[cfg(unix)] hardlink_tracker: &mut HardlinkTracker, #[cfg(unix)] hardlink_scanner: &HardlinkGroupScanner, - #[cfg(not(unix))] _hardlink_tracker: Option<()>, - #[cfg(not(unix))] _hardlink_scanner: Option<()>, verbose: bool, progress_bar: Option<&ProgressBar>, display_manager: Option<&MultiProgress>, @@ -1078,10 +1076,6 @@ fn copy_dir_contents_recursive( hardlink_tracker, #[cfg(unix)] hardlink_scanner, - #[cfg(not(unix))] - _hardlink_tracker, - #[cfg(not(unix))] - _hardlink_scanner, verbose, progress_bar, display_manager, @@ -1099,7 +1093,13 @@ fn copy_dir_contents_recursive( } #[cfg(not(unix))] { - fs::copy(&from_path, &to_path)?; + if from_path.is_symlink() { + // Copy a symlink file (no-follow). + rename_symlink_fallback(&from_path, &to_path)?; + } else { + // Copy a regular file. + fs::copy(&from_path, &to_path)?; + } } // Print verbose message for file @@ -1142,14 +1142,19 @@ fn copy_file_with_hardlinks_helper( return Ok(()); } - // Regular file copy - #[cfg(all(unix, not(any(target_os = "macos", target_os = "redox"))))] - { - fs::copy(from, to).and_then(|_| fsxattr::copy_xattrs(&from, &to))?; - } - #[cfg(any(target_os = "macos", target_os = "redox"))] - { - fs::copy(from, to)?; + if from.is_symlink() { + // Copy a symlink file (no-follow). + rename_symlink_fallback(from, to)?; + } else { + // Copy a regular file. + #[cfg(all(unix, not(any(target_os = "macos", target_os = "redox"))))] + { + fs::copy(from, to).and_then(|_| fsxattr::copy_xattrs(&from, &to))?; + } + #[cfg(any(target_os = "macos", target_os = "redox"))] + { + fs::copy(from, to)?; + } } Ok(()) diff --git a/src/uu/nice/src/nice.rs b/src/uu/nice/src/nice.rs index 8e47e9d078c..fc1e9057bf9 100644 --- a/src/uu/nice/src/nice.rs +++ b/src/uu/nice/src/nice.rs @@ -3,13 +3,14 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (ToDO) getpriority execvp setpriority nstr PRIO cstrs ENOENT +// spell-checker:ignore (ToDO) getpriority setpriority nstr PRIO use clap::{Arg, ArgAction, Command}; -use libc::{PRIO_PROCESS, c_char, c_int, execvp}; -use std::ffi::{CString, OsString}; -use std::io::{Error, Write}; -use std::ptr; +use libc::PRIO_PROCESS; +use std::ffi::OsString; +use std::io::{Error, ErrorKind, Write}; +use std::os::unix::process::CommandExt; +use std::process; use uucore::translate; use uucore::{ @@ -156,21 +157,15 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } } - let cstrs: Vec = matches - .get_many::(options::COMMAND) - .unwrap() - .map(|x| CString::new(x.as_bytes()).unwrap()) - .collect(); + let mut cmd_iter = matches.get_many::(options::COMMAND).unwrap(); + let cmd = cmd_iter.next().unwrap(); + let args: Vec<&String> = cmd_iter.collect(); - let mut args: Vec<*const c_char> = cstrs.iter().map(|s| s.as_ptr()).collect(); - args.push(ptr::null::()); - unsafe { - execvp(args[0], args.as_mut_ptr()); - } + let err = process::Command::new(cmd).args(args).exec(); - show_error!("execvp: {}", Error::last_os_error()); + show_error!("{cmd}: {err}"); - let exit_code = if Error::last_os_error().raw_os_error().unwrap() as c_int == libc::ENOENT { + let exit_code = if err.kind() == ErrorKind::NotFound { 127 } else { 126 diff --git a/src/uu/pr/src/pr.rs b/src/uu/pr/src/pr.rs index f5c5662aa19..843b3b8f970 100644 --- a/src/uu/pr/src/pr.rs +++ b/src/uu/pr/src/pr.rs @@ -48,6 +48,7 @@ mod options { pub const COLUMN_WIDTH: &str = "width"; pub const PAGE_WIDTH: &str = "page-width"; pub const ACROSS: &str = "across"; + pub const COLUMN_DOWN: &str = "column-down"; pub const COLUMN: &str = "column"; pub const COLUMN_CHAR_SEPARATOR: &str = "separator"; pub const COLUMN_STRING_SEPARATOR: &str = "sep-string"; @@ -257,6 +258,13 @@ pub fn uu_app() -> Command { .help(translate!("pr-help-across")) .action(ArgAction::SetTrue), ) + .arg( + // -b is a no-op for backwards compatibility (column-down is now the default) + Arg::new(options::COLUMN_DOWN) + .short('b') + .hide(true) + .action(ArgAction::SetTrue), + ) .arg( Arg::new(options::COLUMN) .long(options::COLUMN) @@ -757,22 +765,29 @@ fn open(path: &str) -> Result, PrError> { |i| { let path_string = path.to_string(); match i.file_type() { - #[cfg(unix)] - ft if ft.is_block_device() => Err(PrError::UnknownFiletype { file: path_string }), - #[cfg(unix)] - ft if ft.is_char_device() => Err(PrError::UnknownFiletype { file: path_string }), - #[cfg(unix)] - ft if ft.is_fifo() => Err(PrError::UnknownFiletype { file: path_string }), #[cfg(unix)] ft if ft.is_socket() => Err(PrError::IsSocket { file: path_string }), ft if ft.is_dir() => Err(PrError::IsDirectory { file: path_string }), - ft if ft.is_file() || ft.is_symlink() => { - Ok(Box::new(File::open(path).map_err(|e| PrError::Input { - source: e, - file: path.to_string(), - })?) as Box) + + ft => { + #[allow(unused_mut)] + let mut is_valid = ft.is_file() || ft.is_symlink(); + + #[cfg(unix)] + { + is_valid = + is_valid || ft.is_char_device() || ft.is_block_device() || ft.is_fifo(); + } + + if is_valid { + Ok(Box::new(File::open(path).map_err(|e| PrError::Input { + source: e, + file: path.to_string(), + })?) as Box) + } else { + Err(PrError::UnknownFiletype { file: path_string }) + } } - _ => Err(PrError::UnknownFiletype { file: path_string }), } }, ) @@ -1165,34 +1180,23 @@ fn header_content(options: &OutputOptions, page: usize) -> Vec { // Use the line width if available, otherwise use default of 72 let total_width = options.line_width.unwrap_or(DEFAULT_COLUMN_WIDTH); - // GNU pr uses a specific layout: - // Date takes up the left part, filename is centered, page is right-aligned let date_len = date_part.chars().count(); let filename_len = filename.chars().count(); let page_len = page_part.chars().count(); let header_line = if date_len + filename_len + page_len + 2 < total_width { - // Check if we're using a custom date format that needs centered alignment - // This preserves backward compatibility while fixing the GNU time-style test - if date_part.starts_with('+') { - // GNU pr uses centered layout for headers with custom date formats - // The filename should be centered between the date and page parts - let space_for_filename = total_width - date_len - page_len; - let padding_before_filename = (space_for_filename - filename_len) / 2; - let padding_after_filename = - space_for_filename - filename_len - padding_before_filename; - - format!( - "{date_part}{:width1$}{filename}{:width2$}{page_part}", - "", - "", - width1 = padding_before_filename, - width2 = padding_after_filename - ) - } else { - // For standard date formats, use simple spacing for backward compatibility - format!("{date_part} {filename} {page_part}") - } + // The filename should be centered between the date and page parts + let space_for_filename = total_width - date_len - page_len; + let padding_before_filename = (space_for_filename - filename_len) / 2; + let padding_after_filename = space_for_filename - filename_len - padding_before_filename; + + format!( + "{date_part}{:width1$}{filename}{:width2$}{page_part}", + "", + "", + width1 = padding_before_filename, + width2 = padding_after_filename + ) } else { // If content is too long, just use single spaces format!("{date_part} {filename} {page_part}") diff --git a/src/uu/rmdir/src/rmdir.rs b/src/uu/rmdir/src/rmdir.rs index 4f13afcbf83..e0c9f73bcec 100644 --- a/src/uu/rmdir/src/rmdir.rs +++ b/src/uu/rmdir/src/rmdir.rs @@ -66,10 +66,11 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { Ok(path.metadata()?.file_type().is_dir()) } - let bytes = path.as_os_str().as_bytes(); + let mut bytes = path.as_os_str().as_bytes(); if error.raw_os_error() == Some(libc::ENOTDIR) && bytes.ends_with(b"/") { // Strip the trailing slash or .symlink_metadata() will follow the symlink - let no_slash: &Path = OsStr::from_bytes(&bytes[..bytes.len() - 1]).as_ref(); + bytes = strip_trailing_slashes_from_path(bytes); + let no_slash: &Path = OsStr::from_bytes(bytes).as_ref(); if no_slash.is_symlink() && points_to_directory(no_slash).unwrap_or(true) { show_error!( "{}", @@ -119,6 +120,15 @@ fn remove_single(path: &Path, opts: Opts) -> Result<(), Error<'_>> { remove_dir(path).map_err(|error| Error { error, path }) } +#[cfg(unix)] +fn strip_trailing_slashes_from_path(path: &[u8]) -> &[u8] { + let mut end = path.len(); + while end > 0 && path[end - 1] == b'/' { + end -= 1; + } + &path[..end] +} + // POSIX: https://pubs.opengroup.org/onlinepubs/009696799/functions/rmdir.html #[cfg(not(windows))] const NOT_EMPTY_CODES: &[i32] = &[libc::ENOTEMPTY, libc::EEXIST]; diff --git a/src/uu/runcon/src/runcon.rs b/src/uu/runcon/src/runcon.rs index 75fdfbec0f1..60c71d1dca3 100644 --- a/src/uu/runcon/src/runcon.rs +++ b/src/uu/runcon/src/runcon.rs @@ -2,7 +2,7 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (vars) RFILE +// spell-checker:ignore (vars) RFILE execv execvp #![cfg(target_os = "linux")] use clap::builder::ValueParser; @@ -48,7 +48,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { .map_err(RunconError::new)?; // On successful execution, the following call never returns, // and this process image is replaced. - execute_command(command, &options.arguments) + // PlainContext mode uses PATH search (like execvp). + execute_command(command, &options.arguments, false) } CommandLineMode::CustomContext { compute_transition_context, @@ -72,7 +73,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { .map_err(RunconError::new)?; // On successful execution, the following call never returns, // and this process image is replaced. - execute_command(command, &options.arguments) + // With -c flag, skip PATH search (like execv vs execvp). + execute_command(command, &options.arguments, *compute_transition_context) } None => print_current_context().map_err(|e| RunconError::new(e).into()), } @@ -367,8 +369,21 @@ fn get_custom_context( /// However, until the *never* type is stabilized, one way to indicate to the /// compiler the only valid return type is to say "if this returns, it will /// always return an error". -fn execute_command(command: &OsStr, arguments: &[OsString]) -> UResult<()> { - let err = process::Command::new(command).args(arguments).exec(); +/// +/// When `skip_path_search` is true (used with `-c` flag), the command is executed +/// without PATH lookup, matching GNU's use of execv() vs execvp(). +fn execute_command(command: &OsStr, arguments: &[OsString], skip_path_search: bool) -> UResult<()> { + // When skip_path_search is true and command has no path separator, + // prepend "./" to prevent PATH lookup (like execv vs execvp). + let command_path = if skip_path_search && !command.as_bytes().contains(&b'/') { + let mut path = OsString::from("./"); + path.push(command); + path + } else { + command.to_os_string() + }; + + let err = process::Command::new(&command_path).args(arguments).exec(); let exit_status = if err.kind() == io::ErrorKind::NotFound { error_exit_status::NOT_FOUND diff --git a/src/uu/shuf/src/shuf.rs b/src/uu/shuf/src/shuf.rs index e69ad1e1caf..4fd5ca85a0f 100644 --- a/src/uu/shuf/src/shuf.rs +++ b/src/uu/shuf/src/shuf.rs @@ -422,7 +422,26 @@ impl Writable for &OsStr { impl Writable for usize { fn write_all_to(&self, output: &mut impl OsWrite) -> Result<(), Error> { - write!(output, "{self}") + let mut n = *self; + + // Handle the zero case explicitly + if n == 0 { + return output.write_all(b"0"); + } + + // Maximum number of digits for u64 is 20 (18446744073709551615) + let mut buf = [0u8; 20]; + let mut i = 20; + + // Write digits from right to left + while n > 0 { + i -= 1; + buf[i] = b'0' + (n % 10) as u8; + n /= 10; + } + + // Write the relevant part of the buffer to output + output.write_all(&buf[i..]) } } diff --git a/src/uu/sort/Cargo.toml b/src/uu/sort/Cargo.toml index 184f6776be7..8a9570eaa30 100644 --- a/src/uu/sort/Cargo.toml +++ b/src/uu/sort/Cargo.toml @@ -60,5 +60,13 @@ name = "sort_bench" harness = false [[bench]] -name = "sort_locale_bench" +name = "sort_locale_c_bench" +harness = false + +[[bench]] +name = "sort_locale_utf8_bench" +harness = false + +[[bench]] +name = "sort_locale_de_bench" harness = false diff --git a/src/uu/sort/benches/sort_bench.rs b/src/uu/sort/benches/sort_bench.rs index a4da0ce6c33..4bd72cf629d 100644 --- a/src/uu/sort/benches/sort_bench.rs +++ b/src/uu/sort/benches/sort_bench.rs @@ -128,6 +128,32 @@ fn sort_numeric(bencher: Bencher, num_lines: usize) { }); } +/// Benchmark general numeric sorting (-g) with decimal and exponent notation +#[divan::bench(args = [200_000])] +fn sort_general_numeric(bencher: Bencher, num_lines: usize) { + let mut data = Vec::new(); + + // Generate numeric data with decimal points and exponents + for i in 0..num_lines { + let int_part = (i * 13) % 100_000; + let frac_part = (i * 7) % 1000; + let exp = (i % 5) as i32 - 2; // -2..=2 + let sign = if i % 2 == 0 { "" } else { "-" }; + data.extend_from_slice(format!("{sign}{int_part}.{frac_part:03}e{exp:+}\n").as_bytes()); + } + + let file_path = setup_test_file(&data); + let output_file = NamedTempFile::new().unwrap(); + let output_path = output_file.path().to_str().unwrap(); + + bencher.bench(|| { + black_box(run_util_function( + uumain, + &["-g", "-o", output_path, file_path.to_str().unwrap()], + )); + }); +} + /// Benchmark reverse sorting with locale-aware data #[divan::bench(args = [500_000])] fn sort_reverse_locale(bencher: Bencher, num_lines: usize) { diff --git a/src/uu/sort/benches/sort_locale_bench.rs b/src/uu/sort/benches/sort_locale_bench.rs deleted file mode 100644 index d00ec9f4ac8..00000000000 --- a/src/uu/sort/benches/sort_locale_bench.rs +++ /dev/null @@ -1,189 +0,0 @@ -// This file is part of the uutils coreutils package. -// -// For the full copyright and license information, please view the LICENSE -// file that was distributed with this source code. - -use divan::{Bencher, black_box}; -use std::env; -use tempfile::NamedTempFile; -use uu_sort::uumain; -use uucore::benchmark::{run_util_function, setup_test_file, text_data}; - -/// Benchmark ASCII-only data sorting with C locale (byte comparison) -#[divan::bench] -fn sort_ascii_c_locale(bencher: Bencher) { - let data = text_data::generate_ascii_data_simple(100_000); - let file_path = setup_test_file(&data); - // Reuse the same output file across iterations to reduce filesystem variance - let output_file = NamedTempFile::new().unwrap(); - let output_path = output_file.path().to_str().unwrap().to_string(); - - bencher.bench(|| { - unsafe { - env::set_var("LC_ALL", "C"); - } - black_box(run_util_function( - uumain, - &["-o", &output_path, file_path.to_str().unwrap()], - )); - }); -} - -/// Benchmark ASCII-only data sorting with UTF-8 locale -#[divan::bench] -fn sort_ascii_utf8_locale(bencher: Bencher) { - let data = text_data::generate_ascii_data_simple(200_000); - let file_path = setup_test_file(&data); - // Reuse the same output file across iterations to reduce filesystem variance - let output_file = NamedTempFile::new().unwrap(); - let output_path = output_file.path().to_str().unwrap().to_string(); - - bencher.bench(|| { - unsafe { - env::set_var("LC_ALL", "en_US.UTF-8"); - } - black_box(run_util_function( - uumain, - &["-o", &output_path, file_path.to_str().unwrap()], - )); - }); -} - -/// Benchmark mixed ASCII/Unicode data with C locale -#[divan::bench] -fn sort_mixed_c_locale(bencher: Bencher) { - let data = text_data::generate_mixed_locale_data(50_000); - let file_path = setup_test_file(&data); - // Reuse the same output file across iterations to reduce filesystem variance - let output_file = NamedTempFile::new().unwrap(); - let output_path = output_file.path().to_str().unwrap().to_string(); - - bencher.bench(|| { - unsafe { - env::set_var("LC_ALL", "C"); - } - black_box(run_util_function( - uumain, - &["-o", &output_path, file_path.to_str().unwrap()], - )); - }); -} - -/// Benchmark mixed ASCII/Unicode data with UTF-8 locale -#[divan::bench] -fn sort_mixed_utf8_locale(bencher: Bencher) { - let data = text_data::generate_mixed_locale_data(50_000); - let file_path = setup_test_file(&data); - // Reuse the same output file across iterations to reduce filesystem variance - let output_file = NamedTempFile::new().unwrap(); - let output_path = output_file.path().to_str().unwrap().to_string(); - - bencher.bench(|| { - unsafe { - env::set_var("LC_ALL", "en_US.UTF-8"); - } - black_box(run_util_function( - uumain, - &["-o", &output_path, file_path.to_str().unwrap()], - )); - }); -} - -/// Benchmark German locale-specific data with C locale -#[divan::bench] -fn sort_german_c_locale(bencher: Bencher) { - let data = text_data::generate_german_locale_data(50_000); - let file_path = setup_test_file(&data); - // Reuse the same output file across iterations to reduce filesystem variance - let output_file = NamedTempFile::new().unwrap(); - let output_path = output_file.path().to_str().unwrap().to_string(); - - bencher.bench(|| { - unsafe { - env::set_var("LC_ALL", "C"); - } - black_box(run_util_function( - uumain, - &["-o", &output_path, file_path.to_str().unwrap()], - )); - }); -} - -/// Benchmark German locale-specific data with German locale -#[divan::bench] -fn sort_german_locale(bencher: Bencher) { - let data = text_data::generate_german_locale_data(50_000); - let file_path = setup_test_file(&data); - // Reuse the same output file across iterations to reduce filesystem variance - let output_file = NamedTempFile::new().unwrap(); - let output_path = output_file.path().to_str().unwrap().to_string(); - - bencher.bench(|| { - unsafe { - env::set_var("LC_ALL", "de_DE.UTF-8"); - } - black_box(run_util_function( - uumain, - &["-o", &output_path, file_path.to_str().unwrap()], - )); - }); -} - -/// Benchmark numeric sorting performance -#[divan::bench] -fn sort_numeric(bencher: Bencher) { - let mut data = Vec::new(); - for i in 0..50_000 { - let line = format!("{}\n", 50_000 - i); - data.extend_from_slice(line.as_bytes()); - } - let file_path = setup_test_file(&data); - - bencher.bench(|| { - unsafe { - env::set_var("LC_ALL", "en_US.UTF-8"); - } - black_box(run_util_function( - uumain, - &["-n", file_path.to_str().unwrap()], - )); - }); -} - -/// Benchmark reverse sorting -#[divan::bench] -fn sort_reverse_mixed(bencher: Bencher) { - let data = text_data::generate_mixed_locale_data(50_000); - let file_path = setup_test_file(&data); - - bencher.bench(|| { - unsafe { - env::set_var("LC_ALL", "en_US.UTF-8"); - } - black_box(run_util_function( - uumain, - &["-r", file_path.to_str().unwrap()], - )); - }); -} - -/// Benchmark unique sorting -#[divan::bench] -fn sort_unique_mixed(bencher: Bencher) { - let data = text_data::generate_mixed_locale_data(50_000); - let file_path = setup_test_file(&data); - - bencher.bench(|| { - unsafe { - env::set_var("LC_ALL", "en_US.UTF-8"); - } - black_box(run_util_function( - uumain, - &["-u", file_path.to_str().unwrap()], - )); - }); -} - -fn main() { - divan::main(); -} diff --git a/src/uu/sort/benches/sort_locale_c_bench.rs b/src/uu/sort/benches/sort_locale_c_bench.rs new file mode 100644 index 00000000000..378a2abb9ac --- /dev/null +++ b/src/uu/sort/benches/sort_locale_c_bench.rs @@ -0,0 +1,72 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +//! Benchmarks for sort with C locale (fast byte-wise comparison). +//! +//! Note: The locale is set in main() BEFORE any benchmark runs because +//! the locale is cached on first access via OnceLock and cannot be changed afterwards. + +use divan::{Bencher, black_box}; +use tempfile::NamedTempFile; +use uu_sort::uumain; +use uucore::benchmark::{run_util_function, setup_test_file, text_data}; + +/// Benchmark ASCII-only data sorting with C locale (byte comparison) +#[divan::bench] +fn sort_ascii_c_locale(bencher: Bencher) { + let data = text_data::generate_ascii_data_simple(100_000); + let file_path = setup_test_file(&data); + let output_file = NamedTempFile::new().unwrap(); + let output_path = output_file.path().to_str().unwrap().to_string(); + + bencher.bench(|| { + black_box(run_util_function( + uumain, + &["-o", &output_path, file_path.to_str().unwrap()], + )); + }); +} + +/// Benchmark mixed ASCII/Unicode data with C locale (byte comparison) +#[divan::bench] +fn sort_mixed_c_locale(bencher: Bencher) { + let data = text_data::generate_mixed_locale_data(50_000); + let file_path = setup_test_file(&data); + let output_file = NamedTempFile::new().unwrap(); + let output_path = output_file.path().to_str().unwrap().to_string(); + + bencher.bench(|| { + black_box(run_util_function( + uumain, + &["-o", &output_path, file_path.to_str().unwrap()], + )); + }); +} + +/// Benchmark German locale-specific data with C locale (byte comparison) +#[divan::bench] +fn sort_german_c_locale(bencher: Bencher) { + let data = text_data::generate_german_locale_data(50_000); + let file_path = setup_test_file(&data); + let output_file = NamedTempFile::new().unwrap(); + let output_path = output_file.path().to_str().unwrap().to_string(); + + bencher.bench(|| { + black_box(run_util_function( + uumain, + &["-o", &output_path, file_path.to_str().unwrap()], + )); + }); +} + +fn main() { + // Set C locale BEFORE any benchmarks run. + // This must happen before divan::main() because the locale is cached + // on first access via OnceLock and cannot be changed afterwards. + unsafe { + std::env::set_var("LC_ALL", "C"); + } + divan::main(); +} diff --git a/src/uu/sort/benches/sort_locale_de_bench.rs b/src/uu/sort/benches/sort_locale_de_bench.rs new file mode 100644 index 00000000000..5c760a694e8 --- /dev/null +++ b/src/uu/sort/benches/sort_locale_de_bench.rs @@ -0,0 +1,40 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +//! Benchmarks for sort with German locale (de_DE.UTF-8 collation). +//! +//! Note: The locale is set in main() BEFORE any benchmark runs because +//! the locale is cached on first access via OnceLock and cannot be changed afterwards. + +use divan::{Bencher, black_box}; +use tempfile::NamedTempFile; +use uu_sort::uumain; +use uucore::benchmark::{run_util_function, setup_test_file, text_data}; + +/// Benchmark German locale-specific data with German locale +#[divan::bench] +fn sort_german_de_locale(bencher: Bencher) { + let data = text_data::generate_german_locale_data(50_000); + let file_path = setup_test_file(&data); + let output_file = NamedTempFile::new().unwrap(); + let output_path = output_file.path().to_str().unwrap().to_string(); + + bencher.bench(|| { + black_box(run_util_function( + uumain, + &["-o", &output_path, file_path.to_str().unwrap()], + )); + }); +} + +fn main() { + // Set German locale BEFORE any benchmarks run. + // This must happen before divan::main() because the locale is cached + // on first access via OnceLock and cannot be changed afterwards. + unsafe { + std::env::set_var("LC_ALL", "de_DE.UTF-8"); + } + divan::main(); +} diff --git a/src/uu/sort/benches/sort_locale_utf8_bench.rs b/src/uu/sort/benches/sort_locale_utf8_bench.rs new file mode 100644 index 00000000000..b0ebb340d99 --- /dev/null +++ b/src/uu/sort/benches/sort_locale_utf8_bench.rs @@ -0,0 +1,102 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +//! Benchmarks for sort with UTF-8 locale (locale-aware collation). +//! +//! Note: The locale is set in main() BEFORE any benchmark runs because +//! the locale is cached on first access via OnceLock and cannot be changed afterwards. + +use divan::{Bencher, black_box}; +use tempfile::NamedTempFile; +use uu_sort::uumain; +use uucore::benchmark::{run_util_function, setup_test_file, text_data}; + +/// Benchmark ASCII-only data sorting with UTF-8 locale +#[divan::bench] +fn sort_ascii_utf8_locale(bencher: Bencher) { + let data = text_data::generate_ascii_data_simple(100_000); + let file_path = setup_test_file(&data); + let output_file = NamedTempFile::new().unwrap(); + let output_path = output_file.path().to_str().unwrap().to_string(); + + bencher.bench(|| { + black_box(run_util_function( + uumain, + &["-o", &output_path, file_path.to_str().unwrap()], + )); + }); +} + +/// Benchmark mixed ASCII/Unicode data with UTF-8 locale +#[divan::bench] +fn sort_mixed_utf8_locale(bencher: Bencher) { + let data = text_data::generate_mixed_locale_data(50_000); + let file_path = setup_test_file(&data); + let output_file = NamedTempFile::new().unwrap(); + let output_path = output_file.path().to_str().unwrap().to_string(); + + bencher.bench(|| { + black_box(run_util_function( + uumain, + &["-o", &output_path, file_path.to_str().unwrap()], + )); + }); +} + +/// Benchmark numeric sorting with UTF-8 locale +#[divan::bench] +fn sort_numeric_utf8_locale(bencher: Bencher) { + let mut data = Vec::new(); + for i in 0..50_000 { + let line = format!("{}\n", 50_000 - i); + data.extend_from_slice(line.as_bytes()); + } + let file_path = setup_test_file(&data); + + bencher.bench(|| { + black_box(run_util_function( + uumain, + &["-n", file_path.to_str().unwrap()], + )); + }); +} + +/// Benchmark reverse sorting with UTF-8 locale +#[divan::bench] +fn sort_reverse_utf8_locale(bencher: Bencher) { + let data = text_data::generate_mixed_locale_data(50_000); + let file_path = setup_test_file(&data); + + bencher.bench(|| { + black_box(run_util_function( + uumain, + &["-r", file_path.to_str().unwrap()], + )); + }); +} + +/// Benchmark unique sorting with UTF-8 locale +#[divan::bench] +fn sort_unique_utf8_locale(bencher: Bencher) { + let data = text_data::generate_mixed_locale_data(50_000); + let file_path = setup_test_file(&data); + + bencher.bench(|| { + black_box(run_util_function( + uumain, + &["-u", file_path.to_str().unwrap()], + )); + }); +} + +fn main() { + // Set UTF-8 locale BEFORE any benchmarks run. + // This must happen before divan::main() because the locale is cached + // on first access via OnceLock and cannot be changed afterwards. + unsafe { + std::env::set_var("LC_ALL", "en_US.UTF-8"); + } + divan::main(); +} diff --git a/src/uu/split/locales/en-US.ftl b/src/uu/split/locales/en-US.ftl index 4247eb5b9d9..629b8956d75 100644 --- a/src/uu/split/locales/en-US.ftl +++ b/src/uu/split/locales/en-US.ftl @@ -43,6 +43,7 @@ split-error-unable-to-reopen-file = unable to re-open { $file }; aborting split-error-file-descriptor-limit = at file descriptor limit, but no file descriptor left to close. Closed { $count } writers before. split-error-shell-process-returned = Shell process returned { $code } split-error-shell-process-terminated = Shell process terminated by signal +split-error-is-a-directory = { $dir }: Is a directory # Help messages for command-line options split-help-bytes = put SIZE bytes per output file diff --git a/src/uu/split/src/platform/unix.rs b/src/uu/split/src/platform/unix.rs index d1257954d3e..656bd0109be 100644 --- a/src/uu/split/src/platform/unix.rs +++ b/src/uu/split/src/platform/unix.rs @@ -4,8 +4,8 @@ // file that was distributed with this source code. use std::env; use std::ffi::OsStr; -use std::io::Write; use std::io::{BufWriter, Error, Result}; +use std::io::{ErrorKind, Write}; use std::path::Path; use std::process::{Child, Command, Stdio}; use uucore::error::USimpleError; @@ -43,9 +43,9 @@ impl Write for FilterWriter { /// Have an environment variable set at a value during this lifetime struct WithEnvVarSet { /// Env var key - _previous_var_key: String, + previous_var_key: String, /// Previous value set to this key - _previous_var_value: std::result::Result, + previous_var_value: std::result::Result, } impl WithEnvVarSet { /// Save previous value assigned to key, set key=value @@ -55,8 +55,8 @@ impl WithEnvVarSet { env::set_var(key, value); } Self { - _previous_var_key: String::from(key), - _previous_var_value: previous_env_value, + previous_var_key: String::from(key), + previous_var_value: previous_env_value, } } } @@ -64,13 +64,13 @@ impl WithEnvVarSet { impl Drop for WithEnvVarSet { /// Restore previous value now that this is being dropped by context fn drop(&mut self) { - if let Ok(ref prev_value) = self._previous_var_value { + if let Ok(ref prev_value) = self.previous_var_value { unsafe { - env::set_var(&self._previous_var_key, prev_value); + env::set_var(&self.previous_var_key, prev_value); } } else { unsafe { - env::remove_var(&self._previous_var_key); + env::remove_var(&self.previous_var_key); } } } @@ -139,10 +139,13 @@ pub fn instantiate_current_writer( .create(true) .truncate(true) .open(Path::new(&filename)) - .map_err(|_| { - Error::other( + .map_err(|e| match e.kind() { + ErrorKind::IsADirectory => Error::other( + translate!("split-error-is-a-directory", "dir" => filename), + ), + _ => Error::other( translate!("split-error-unable-to-open-file", "file" => filename), - ) + ), })? } else { // re-open file that we previously created to append to it diff --git a/src/uu/split/src/platform/windows.rs b/src/uu/split/src/platform/windows.rs index e443a9cfb3b..6693e4fe909 100644 --- a/src/uu/split/src/platform/windows.rs +++ b/src/uu/split/src/platform/windows.rs @@ -3,8 +3,8 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. use std::ffi::OsStr; -use std::io::Write; use std::io::{BufWriter, Error, Result}; +use std::io::{ErrorKind, Write}; use std::path::Path; use uucore::fs; use uucore::translate; @@ -25,8 +25,13 @@ pub fn instantiate_current_writer( .create(true) .truncate(true) .open(Path::new(&filename)) - .map_err(|_| { - Error::other(translate!("split-error-unable-to-open-file", "file" => filename)) + .map_err(|e| match e.kind() { + ErrorKind::IsADirectory => { + Error::other(translate!("split-error-is-a-directory", "dir" => filename)) + } + _ => { + Error::other(translate!("split-error-unable-to-open-file", "file" => filename)) + } })? } else { // re-open file that we previously created to append to it diff --git a/src/uu/stat/src/stat.rs b/src/uu/stat/src/stat.rs index 327e89a6888..48430a26157 100644 --- a/src/uu/stat/src/stat.rs +++ b/src/uu/stat/src/stat.rs @@ -9,7 +9,7 @@ use uucore::translate; use clap::builder::ValueParser; use uucore::display::Quotable; -use uucore::fs::display_permissions; +use uucore::fs::{display_permissions, major, minor}; use uucore::fsext::{ FsMeta, MetadataTimeField, StatFs, metadata_get_time, pretty_filetype, pretty_fstype, read_fs_list, statfs, @@ -70,6 +70,8 @@ struct Flags { space: bool, sign: bool, group: bool, + major: bool, + minor: bool, } /// checks if the string is within the specified bound, @@ -739,7 +741,6 @@ impl Stater { return Ok(Token::Char('%')); } if chars[*i] == '%' { - *i += 1; return Ok(Token::Char('%')); } @@ -794,13 +795,14 @@ impl Stater { if let Some(&next_char) = chars.get(*i + 1) { if (chars[*i] == 'H' || chars[*i] == 'L') && (next_char == 'd' || next_char == 'r') { - let specifier = format!("{}{next_char}", chars[*i]); + flag.major = chars[*i] == 'H'; + flag.minor = chars[*i] == 'L'; *i += 1; return Ok(Token::Directive { flag, width, precision, - format: specifier.chars().next().unwrap(), + format: next_char, }); } } @@ -908,6 +910,28 @@ impl Stater { Ok(tokens) } + fn populate_mount_list() -> UResult> { + let mut mount_list = read_fs_list() + .map_err(|e| { + USimpleError::new( + e.code(), + StatError::CannotReadFilesystem { + error: e.to_string(), + } + .to_string(), + ) + })? + .iter() + .map(|mi| mi.mount_dir.clone()) + .collect::>(); + + // Reverse sort. The longer comes first. + mount_list.sort(); + mount_list.reverse(); + + Ok(mount_list) + } + fn new(matches: &ArgMatches) -> UResult { let files: Vec = matches .get_many::(options::FILES) @@ -938,27 +962,16 @@ impl Stater { let default_dev_tokens = Self::generate_tokens(&Self::default_format(show_fs, terse, true), use_printf)?; - let mount_list = if show_fs { - // mount points aren't displayed when showing filesystem information + // mount points aren't displayed when showing filesystem information, or + // whenever the format string does not request the mount point. + let mount_list = if show_fs + || !default_tokens + .iter() + .any(|tok| matches!(tok, Token::Directive { format: 'm', .. })) + { None } else { - let mut mount_list = read_fs_list() - .map_err(|e| { - USimpleError::new( - e.code(), - StatError::CannotReadFilesystem { - error: e.to_string(), - } - .to_string(), - ) - })? - .iter() - .map(|mi| mi.mount_dir.clone()) - .collect::>(); - // Reverse sort. The longer comes first. - mount_list.sort(); - mount_list.reverse(); - Some(mount_list) + Some(Self::populate_mount_list()?) }; Ok(Self { @@ -1004,7 +1017,8 @@ impl Stater { file: &OsString, file_type: &FileType, from_user: bool, - _follow_symbolic_links: bool, + #[cfg(feature = "selinux")] follow_symbolic_links: bool, + #[cfg(not(feature = "selinux"))] _: bool, ) -> Result<(), i32> { match *t { Token::Byte(byte) => write_raw_byte(byte), @@ -1035,7 +1049,7 @@ impl Stater { if uucore::selinux::is_selinux_enabled() { match uucore::selinux::get_selinux_security_context( Path::new(file), - _follow_symbolic_links, + follow_symbolic_links, ) { Ok(ctx) => OutputType::Str(ctx), Err(_) => OutputType::Str(translate!( @@ -1052,6 +1066,8 @@ impl Stater { } } // device number in decimal + 'd' if flag.major => OutputType::Unsigned(major(meta.dev() as _) as u64), + 'd' if flag.minor => OutputType::Unsigned(minor(meta.dev() as _) as u64), 'd' => OutputType::Unsigned(meta.dev()), // device number in hex 'D' => OutputType::UnsignedHex(meta.dev()), @@ -1090,10 +1106,10 @@ impl Stater { 's' => OutputType::Integer(meta.len() as i64), // major device type in hex, for character/block device special // files - 't' => OutputType::UnsignedHex(meta.rdev() >> 8), + 't' => OutputType::UnsignedHex(major(meta.rdev() as _) as u64), // minor device type in hex, for character/block device special // files - 'T' => OutputType::UnsignedHex(meta.rdev() & 0xff), + 'T' => OutputType::UnsignedHex(minor(meta.rdev() as _) as u64), // user ID of owner 'u' => OutputType::Unsigned(meta.uid() as u64), // user name of owner @@ -1136,15 +1152,10 @@ impl Stater { .map_or((0, 0), system_time_to_sec); OutputType::Float(sec as f64 + nsec as f64 / 1_000_000_000.0) } - 'R' => { - let major = meta.rdev() >> 8; - let minor = meta.rdev() & 0xff; - OutputType::Str(format!("{major},{minor}")) - } + 'R' => OutputType::UnsignedHex(meta.rdev()), + 'r' if flag.major => OutputType::Unsigned(major(meta.rdev() as _) as u64), + 'r' if flag.minor => OutputType::Unsigned(minor(meta.rdev() as _) as u64), 'r' => OutputType::Unsigned(meta.rdev()), - 'H' => OutputType::Unsigned(meta.rdev() >> 8), // Major in decimal - 'L' => OutputType::Unsigned(meta.rdev() & 0xff), // Minor in decimal - _ => OutputType::Unknown, }; print_it(&output, flag, width, precision); @@ -1269,7 +1280,7 @@ impl Stater { } else { let device_line = if show_dev_type { format!( - "{}: %Dh/%dd\t{}: %-10i {}: %-5h {} {}: %t,%T\n", + "{}: %Hd,%Ld\t{}: %-10i {}: %-5h {} {}: %t,%T\n", translate!("stat-word-device"), translate!("stat-word-inode"), translate!("stat-word-links"), @@ -1278,7 +1289,7 @@ impl Stater { ) } else { format!( - "{}: %Dh/%dd\t{}: %-10i {}: %h\n", + "{}: %Hd,%Ld\t{}: %-10i {}: %h\n", translate!("stat-word-device"), translate!("stat-word-inode"), translate!("stat-word-links") diff --git a/src/uu/stdbuf/src/libstdbuf/Cargo.toml b/src/uu/stdbuf/src/libstdbuf/Cargo.toml index 6460c441eec..8a92fcbb56a 100644 --- a/src/uu/stdbuf/src/libstdbuf/Cargo.toml +++ b/src/uu/stdbuf/src/libstdbuf/Cargo.toml @@ -10,6 +10,9 @@ keywords.workspace = true categories.workspace = true edition.workspace = true +[lints] +workspace = true + [lib] name = "stdbuf" path = "src/libstdbuf.rs" diff --git a/src/uu/stty/src/stty.rs b/src/uu/stty/src/stty.rs index d60d4d985ba..8808857b630 100644 --- a/src/uu/stty/src/stty.rs +++ b/src/uu/stty/src/stty.rs @@ -26,12 +26,12 @@ use nix::sys::termios::{ use nix::{ioctl_read_bad, ioctl_write_ptr_bad}; use std::cmp::Ordering; use std::fs::File; -use std::io::{self, Stdout, stdout}; +use std::io::{self, Stdin, stdin, stdout}; use std::num::IntErrorKind; use std::os::fd::{AsFd, BorrowedFd}; use std::os::unix::fs::OpenOptionsExt; use std::os::unix::io::{AsRawFd, RawFd}; -use uucore::error::{UError, UResult, USimpleError, UUsageError}; +use uucore::error::{FromIo, UError, UResult, USimpleError, UUsageError}; use uucore::format_usage; use uucore::parser::num_parser::ExtendedParser; use uucore::translate; @@ -124,12 +124,13 @@ struct Options<'a> { all: bool, save: bool, file: Device, + device_name: String, settings: Option>, } enum Device { File(File), - Stdout(Stdout), + Stdin(Stdin), } #[derive(Debug)] @@ -166,7 +167,7 @@ impl AsFd for Device { fn as_fd(&self) -> BorrowedFd<'_> { match self { Self::File(f) => f.as_fd(), - Self::Stdout(stdout) => stdout.as_fd(), + Self::Stdin(stdin) => stdin.as_fd(), } } } @@ -175,45 +176,42 @@ impl AsRawFd for Device { fn as_raw_fd(&self) -> RawFd { match self { Self::File(f) => f.as_raw_fd(), - Self::Stdout(stdout) => stdout.as_raw_fd(), + Self::Stdin(stdin) => stdin.as_raw_fd(), } } } impl<'a> Options<'a> { fn from(matches: &'a ArgMatches) -> io::Result { - Ok(Self { - all: matches.get_flag(options::ALL), - save: matches.get_flag(options::SAVE), - file: match matches.get_one::(options::FILE) { - // Two notes here: - // 1. O_NONBLOCK is needed because according to GNU docs, a - // POSIX tty can block waiting for carrier-detect if the - // "clocal" flag is not set. If your TTY is not connected - // to a modem, it is probably not relevant though. - // 2. We never close the FD that we open here, but the OS - // will clean up the FD for us on exit, so it doesn't - // matter. The alternative would be to have an enum of - // BorrowedFd/OwnedFd to handle both cases. - Some(f) => Device::File( + let (file, device_name) = match matches.get_one::(options::FILE) { + // Two notes here: + // 1. O_NONBLOCK is needed because according to GNU docs, a + // POSIX tty can block waiting for carrier-detect if the + // "clocal" flag is not set. If your TTY is not connected + // to a modem, it is probably not relevant though. + // 2. We never close the FD that we open here, but the OS + // will clean up the FD for us on exit, so it doesn't + // matter. The alternative would be to have an enum of + // BorrowedFd/OwnedFd to handle both cases. + Some(f) => ( + Device::File( std::fs::OpenOptions::new() .read(true) .custom_flags(O_NONBLOCK) .open(f)?, ), - // default to /dev/tty, if that does not exist then default to stdout - None => { - if let Ok(f) = std::fs::OpenOptions::new() - .read(true) - .custom_flags(O_NONBLOCK) - .open("/dev/tty") - { - Device::File(f) - } else { - Device::Stdout(stdout()) - } - } - }, + f.clone(), + ), + // Per POSIX, stdin is used for TTY operations when no device is specified. + // This matches GNU coreutils behavior: if stdin is not a TTY, + // tcgetattr will fail with "Inappropriate ioctl for device". + None => (Device::Stdin(stdin()), "standard input".to_string()), + }; + Ok(Self { + all: matches.get_flag(options::ALL), + save: matches.get_flag(options::SAVE), + file, + device_name, settings: matches .get_many::(options::SETTINGS) .map(|v| v.map(|s| s.as_ref()).collect()), @@ -412,8 +410,8 @@ fn stty(opts: &Options) -> UResult<()> { } } - // TODO: Figure out the right error message for when tcgetattr fails - let mut termios = tcgetattr(opts.file.as_fd())?; + let mut termios = + tcgetattr(opts.file.as_fd()).map_err_context(|| opts.device_name.clone())?; // iterate over valid_args, match on the arg type, do the matching apply function for arg in &valid_args { @@ -433,8 +431,7 @@ fn stty(opts: &Options) -> UResult<()> { } tcsetattr(opts.file.as_fd(), set_arg, &termios)?; } else { - // TODO: Figure out the right error message for when tcgetattr fails - let termios = tcgetattr(opts.file.as_fd())?; + let termios = tcgetattr(opts.file.as_fd()).map_err_context(|| opts.device_name.clone())?; print_settings(&termios, opts)?; } Ok(()) @@ -997,7 +994,7 @@ fn apply_char_mapping(termios: &mut Termios, mapping: &(S, u8)) { /// /// The state array contains: /// - `state[0]`: input flags -/// - `state[1]`: output flags +/// - `state[1]`: output flags /// - `state[2]`: control flags /// - `state[3]`: local flags /// - `state[4..]`: control characters (optional) @@ -1036,11 +1033,15 @@ fn apply_special_setting( match setting { SpecialSetting::Rows(n) => size.rows = *n, SpecialSetting::Cols(n) => size.columns = *n, - SpecialSetting::Line(_n) => { + #[cfg_attr( + not(any(target_os = "linux", target_os = "android")), + expect(unused_variables) + )] + SpecialSetting::Line(n) => { // nix only defines Termios's `line_discipline` field on these platforms #[cfg(any(target_os = "linux", target_os = "android"))] { - _termios.line_discipline = *_n; + _termios.line_discipline = *n; } } } diff --git a/src/uu/tac/locales/en-US.ftl b/src/uu/tac/locales/en-US.ftl index 3c849c4d712..2632aa3dbb8 100644 --- a/src/uu/tac/locales/en-US.ftl +++ b/src/uu/tac/locales/en-US.ftl @@ -6,7 +6,7 @@ tac-help-separator = use STRING as the separator instead of newline # Error messages tac-error-invalid-regex = invalid regular expression: { $error } -tac-error-invalid-argument = { $argument }: read error: Invalid argument +tac-error-invalid-directory-argument = { $argument }: read error: Is a directory tac-error-file-not-found = failed to open { $filename } for reading: No such file or directory tac-error-read-error = failed to read from { $filename }: { $error } tac-error-write-error = failed to write to stdout: { $error } diff --git a/src/uu/tac/locales/fr-FR.ftl b/src/uu/tac/locales/fr-FR.ftl index f49a39e8d19..6c56de6283a 100644 --- a/src/uu/tac/locales/fr-FR.ftl +++ b/src/uu/tac/locales/fr-FR.ftl @@ -6,7 +6,7 @@ tac-help-separator = utiliser CHAÎNE comme séparateur au lieu du saut de ligne # Messages d'erreur tac-error-invalid-regex = expression régulière invalide : { $error } -tac-error-invalid-argument = { $argument } : erreur de lecture : Argument invalide tac-error-file-not-found = échec de l'ouverture de { $filename } en lecture : Aucun fichier ou répertoire de ce type tac-error-read-error = échec de la lecture depuis { $filename } : { $error } tac-error-write-error = échec de l'écriture vers stdout : { $error } +tac-error-invalid-directory-argument = { $argument } : erreur de lecture : Est un répertoire diff --git a/src/uu/tac/src/error.rs b/src/uu/tac/src/error.rs index 133a46266a0..098e997d4af 100644 --- a/src/uu/tac/src/error.rs +++ b/src/uu/tac/src/error.rs @@ -15,9 +15,9 @@ pub enum TacError { /// A regular expression given by the user is invalid. #[error("{}", translate!("tac-error-invalid-regex", "error" => .0))] InvalidRegex(regex::Error), - /// An argument to tac is invalid. - #[error("{}", translate!("tac-error-invalid-argument", "argument" => .0.maybe_quote()))] - InvalidArgument(OsString), + /// The argument to tac is a directory. + #[error("{}", translate!("tac-error-invalid-directory-argument", "argument" => .0.maybe_quote()))] + InvalidDirectoryArgument(OsString), /// The specified file is not found on the filesystem. #[error("{}", translate!("tac-error-file-not-found", "filename" => .0.quote()))] FileNotFound(OsString), diff --git a/src/uu/tac/src/tac.rs b/src/uu/tac/src/tac.rs index 507dd153199..f38661d03e9 100644 --- a/src/uu/tac/src/tac.rs +++ b/src/uu/tac/src/tac.rs @@ -253,7 +253,8 @@ fn tac(filenames: &[OsString], before: bool, regex: bool, separator: &str) -> UR } else { let path = Path::new(filename); if path.is_dir() { - let e: Box = TacError::InvalidArgument(filename.clone()).into(); + let e: Box = + TacError::InvalidDirectoryArgument(filename.clone()).into(); show!(e); continue; } diff --git a/src/uu/tail/src/follow/watch.rs b/src/uu/tail/src/follow/watch.rs index 95f38aabc67..b4b4d00acf4 100644 --- a/src/uu/tail/src/follow/watch.rs +++ b/src/uu/tail/src/follow/watch.rs @@ -47,9 +47,7 @@ impl WatcherRx { Tested for notify::InotifyWatcher and for notify::PollWatcher. */ if let Some(parent) = path.parent() { - // clippy::assigning_clones added with Rust 1.78 - // Rust version = 1.76 on OpenBSD stable/7.5 - #[cfg_attr(not(target_os = "openbsd"), allow(clippy::assigning_clones))] + #[allow(clippy::assigning_clones)] if parent.is_dir() { path = parent.to_owned(); } else { diff --git a/src/uu/tail/src/paths.rs b/src/uu/tail/src/paths.rs index 340a0b29dec..3f37091d8cd 100644 --- a/src/uu/tail/src/paths.rs +++ b/src/uu/tail/src/paths.rs @@ -179,10 +179,10 @@ impl MetadataExtTail for Metadata { Ok(other.len() < self.len() && other.modified()? != self.modified()?) } - fn file_id_eq(&self, _other: &Metadata) -> bool { + fn file_id_eq(&self, #[cfg(unix)] other: &Metadata, #[cfg(not(unix))] _: &Metadata) -> bool { #[cfg(unix)] { - self.ino().eq(&_other.ino()) + self.ino().eq(&other.ino()) } #[cfg(windows)] { diff --git a/src/uu/test/src/parser.rs b/src/uu/test/src/parser.rs index 167bf77023a..c1c06e4c581 100644 --- a/src/uu/test/src/parser.rs +++ b/src/uu/test/src/parser.rs @@ -188,7 +188,16 @@ impl Parser { match symbol { Symbol::LParen => self.lparen()?, Symbol::Bang => self.bang()?, - Symbol::UnaryOp(_) => self.uop(symbol), + Symbol::UnaryOp(_) => { + // Three-argument string comparison: `-f = a` means "-f" = "a", not file test + let is_string_cmp = matches!(self.peek(), Symbol::Op(Operator::String(_))) + && !matches!(Symbol::new(self.tokens.clone().nth(1)), Symbol::None); + if is_string_cmp { + self.literal(symbol.into_literal())?; + } else { + self.uop(symbol); + } + } Symbol::None => self.stack.push(symbol), literal => self.literal(literal)?, } diff --git a/src/uu/touch/src/touch.rs b/src/uu/touch/src/touch.rs index e00c1df8256..bde22ab336a 100644 --- a/src/uu/touch/src/touch.rs +++ b/src/uu/touch/src/touch.rs @@ -3,7 +3,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (ToDO) filetime datetime lpszfilepath mktime DATETIME datelike timelike +// spell-checker:ignore (ToDO) filetime datetime lpszfilepath mktime DATETIME datelike timelike UTIME // spell-checker:ignore (FORMATS) MMDDhhmm YYYYMMDDHHMM YYMMDDHHMM YYYYMMDDHHMMS pub mod error; @@ -23,6 +23,8 @@ use std::io::{Error, ErrorKind}; use std::path::{Path, PathBuf}; use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError}; +#[cfg(target_os = "linux")] +use uucore::libc; use uucore::parser::shortcut_value_parser::ShortcutValueParser; use uucore::translate; use uucore::{format_usage, show}; @@ -377,7 +379,19 @@ pub fn touch(files: &[InputFile], opts: &Options) -> Result<(), TouchError> { (atime, mtime) } Source::Now => { - let now = datetime_to_filetime(&Local::now()); + let now: FileTime; + #[cfg(target_os = "linux")] + { + if opts.date.is_none() { + now = FileTime::from_unix_time(0, libc::UTIME_NOW as u32); + } else { + now = datetime_to_filetime(&Local::now()); + } + } + #[cfg(not(target_os = "linux"))] + { + now = datetime_to_filetime(&Local::now()); + } (now, now) } &Source::Timestamp(ts) => (ts, ts), diff --git a/src/uu/tsort/Cargo.toml b/src/uu/tsort/Cargo.toml index 94b170223c1..72559199c8c 100644 --- a/src/uu/tsort/Cargo.toml +++ b/src/uu/tsort/Cargo.toml @@ -1,3 +1,4 @@ +#spell-checker:ignore (libs) interner [package] name = "uu_tsort" description = "tsort ~ (uutils) topologically sort input (partially ordered) pairs" @@ -19,9 +20,11 @@ path = "src/tsort.rs" [dependencies] clap = { workspace = true } +fluent = { workspace = true } +string-interner = { workspace = true } thiserror = { workspace = true } +nix = { workspace = true, features = ["fs"] } uucore = { workspace = true } -fluent = { workspace = true } [[bin]] name = "tsort" diff --git a/src/uu/tsort/src/tsort.rs b/src/uu/tsort/src/tsort.rs index 26c6f8ffc99..713c2f5c907 100644 --- a/src/uu/tsort/src/tsort.rs +++ b/src/uu/tsort/src/tsort.rs @@ -2,108 +2,104 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -//spell-checker:ignore TAOCP indegree +//spell-checker:ignore TAOCP indegree fadvise FADV +//spell-checker:ignore (libs) interner uclibc use clap::{Arg, ArgAction, Command}; use std::collections::hash_map::Entry; use std::collections::{HashMap, VecDeque}; use std::ffi::OsString; -use std::path::Path; +use std::fs::File; +use std::io::{self, BufRead, BufReader}; +use string_interner::StringInterner; +use string_interner::backend::BucketBackend; use thiserror::Error; use uucore::display::Quotable; use uucore::error::{UError, UResult, USimpleError}; -use uucore::{format_usage, show}; +use uucore::{format_usage, show, translate}; -use uucore::translate; +// short types for switching interning behavior on the fly. +type Sym = string_interner::symbol::SymbolUsize; +type Interner = StringInterner>; mod options { pub const FILE: &str = "file"; } -#[derive(Debug, Error)] -enum TsortError { - /// The input file is actually a directory. - #[error("{input}: {message}", input = .0.maybe_quote(), message = translate!("tsort-error-is-dir"))] - IsDir(OsString), - - /// The number of tokens in the input data is odd. - /// - /// The list of edges must be even because each edge has two - /// components: a source node and a target node. - #[error("{input}: {message}", input = .0.maybe_quote(), message = translate!("tsort-error-odd"))] - NumTokensOdd(OsString), - - /// The graph contains a cycle. - #[error("{input}: {message}", input = .0.maybe_quote(), message = translate!("tsort-error-loop"))] - Loop(OsString), -} - -// Auxiliary struct, just for printing loop nodes via show! macro -#[derive(Debug, Error)] -#[error("{0}")] -struct LoopNode<'a>(&'a str); - -impl UError for TsortError {} -impl UError for LoopNode<'_> {} - #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?; - let mut inputs: Vec = matches + let mut inputs = matches .get_many::(options::FILE) - .map(|vals| vals.cloned().collect()) - .unwrap_or_default(); - - if inputs.is_empty() { - inputs.push(OsString::from("-")); - } - - if inputs.len() > 1 { - return Err(USimpleError::new( - 1, - translate!( - "tsort-error-extra-operand", - "operand" => inputs[1].quote(), - "util" => uucore::util_name() - ), - )); - } - - let input = inputs .into_iter() - .next() - .expect(translate!("tsort-error-at-least-one-input").as_str()); - - let data = if input == "-" { - let stdin = std::io::stdin(); - std::io::read_to_string(stdin)? - } else { - let path = Path::new(&input); - if path.is_dir() { - return Err(TsortError::IsDir(input.clone()).into()); + .flatten(); + + let input = match (inputs.next(), inputs.next()) { + (None, _) => { + return Err(USimpleError::new( + 1, + translate!("tsort-error-at-least-one-input"), + )); + } + (Some(input), None) => input, + (Some(_), Some(extra)) => { + return Err(USimpleError::new( + 1, + translate!( + "tsort-error-extra-operand", + "operand" => extra.quote(), + "util" => uucore::util_name() + ), + )); } - std::fs::read_to_string(path)? }; - + let file: File; // Create the directed graph from pairs of tokens in the input data. - let mut g = Graph::new(input.clone()); - // Input is considered to be in the format - // From1 To1 From2 To2 ... - // with tokens separated by whitespaces - let mut edge_tokens = data.split_whitespace(); - // Note: this is equivalent to iterating over edge_tokens.chunks(2) - // but chunks() exists only for slices and would require unnecessary Vec allocation. - // Itertools::chunks() is not used due to unnecessary overhead for internal RefCells - loop { - // Try take next pair of tokens - let Some(from) = edge_tokens.next() else { - // no more tokens -> end of input. Graph constructed - break; - }; - let Some(to) = edge_tokens.next() else { - return Err(TsortError::NumTokensOdd(input.clone()).into()); - }; - g.add_edge(from, to); + let mut g = Graph::new(input.to_string_lossy().to_string()); + if input == "-" { + process_input(io::stdin().lock(), &mut g)?; + } else { + // Windows reports a permission denied error when trying to read a directory. + // So we check manually beforehand. On other systems, we avoid this extra check for performance. + #[cfg(windows)] + { + use std::path::Path; + + let path = Path::new(input); + if path.is_dir() { + return Err(TsortError::IsDir(input.to_string_lossy().to_string()).into()); + } + + file = File::open(path)?; + } + #[cfg(not(windows))] + { + file = File::open(input)?; + + // advise the OS we will access the data sequentially if available. + #[cfg(any( + target_os = "linux", + target_os = "android", + target_os = "fuchsia", + target_os = "wasi", + target_env = "uclibc", + target_os = "freebsd", + ))] + { + use nix::fcntl::{PosixFadviseAdvice, posix_fadvise}; + use std::os::unix::io::AsFd; + + posix_fadvise( + file.as_fd(), + 0, // offset 0 => from the start of the file + 0, // length 0 => for the whole file + PosixFadviseAdvice::POSIX_FADV_SEQUENTIAL, + ) + .ok(); + } + } + let reader = BufReader::new(file); + process_input(reader, &mut g)?; } g.run_tsort(); @@ -117,6 +113,7 @@ pub fn uu_app() -> Command { .override_usage(format_usage(&translate!("tsort-usage"))) .about(translate!("tsort-about")) .infer_long_args(true) + // no-op flag, needed for POSIX compatibility. .arg( Arg::new("warn") .short('w') @@ -128,11 +125,75 @@ pub fn uu_app() -> Command { .hide(true) .value_parser(clap::value_parser!(OsString)) .value_hint(clap::ValueHint::FilePath) - .num_args(0..) + .default_value("-") + .num_args(1..) .action(ArgAction::Append), ) } +#[derive(Debug, Error)] +enum TsortError { + /// The input file is actually a directory. + #[error("{input}: {message}", input = .0.maybe_quote(), message = translate!("tsort-error-is-dir"))] + IsDir(String), + + /// The number of tokens in the input data is odd. + /// + /// The length of the list of edges must be even because each edge has two + /// components: a source node and a target node. + #[error("{input}: {message}", input = .0.maybe_quote(), message = translate!("tsort-error-odd"))] + NumTokensOdd(String), + + /// The graph contains a cycle. + #[error("{input}: {message}", input = .0, message = translate!("tsort-error-loop"))] + Loop(String), + + /// Wrapper for bubbling up IO errors + #[error("{0}")] + IO(#[from] std::io::Error), +} + +// Auxiliary struct, just for printing loop nodes via show! macro +#[derive(Debug, Error)] +#[error("{0}")] +struct LoopNode<'a>(&'a str); + +impl UError for TsortError {} +impl UError for LoopNode<'_> {} + +fn process_input(reader: R, graph: &mut Graph) -> Result<(), TsortError> { + let mut pending: Option = None; + + // Input is considered to be in the format + // From1 To1 From2 To2 ... + // with tokens separated by whitespaces + + for line in reader.lines() { + let line = line.map_err(|e| { + if e.kind() == io::ErrorKind::IsADirectory { + TsortError::IsDir(graph.name()) + } else { + e.into() + } + })?; + for token in line.split_whitespace() { + // Intern the token and get a Sym + let token_sym = graph.interner.get_or_intern(token); + + if let Some(from) = pending.take() { + graph.add_edge(from, token_sym); + } else { + pending = Some(token_sym); + } + } + } + if pending.is_some() { + return Err(TsortError::NumTokensOdd(graph.name())); + } + + Ok(()) +} + /// Find the element `x` in `vec` and remove it, returning its index. fn remove(vec: &mut Vec, x: T) -> Option where @@ -143,40 +204,54 @@ where }) } -// We use String as a representation of node here -// but using integer may improve performance. +#[derive(Clone, Copy, PartialEq, Eq)] +enum VisitedState { + Opened, + Closed, +} + #[derive(Default)] -struct Node<'input> { - successor_names: Vec<&'input str>, +struct Node { + successor_tokens: Vec, predecessor_count: usize, } -impl<'input> Node<'input> { - fn add_successor(&mut self, successor_name: &'input str) { - self.successor_names.push(successor_name); +impl Node { + fn add_successor(&mut self, successor_name: Sym) { + self.successor_tokens.push(successor_name); } } -struct Graph<'input> { - name: OsString, - nodes: HashMap<&'input str, Node<'input>>, +struct Graph { + name_sym: Sym, + nodes: HashMap, + interner: Interner, } -#[derive(Clone, Copy, PartialEq, Eq)] -enum VisitedState { - Opened, - Closed, -} - -impl<'input> Graph<'input> { - fn new(name: OsString) -> Self { +impl Graph { + fn new(name: String) -> Self { + let mut interner = Interner::new(); + let name_sym = interner.get_or_intern(name); Self { - name, + name_sym, + interner, nodes: HashMap::default(), } } - fn add_edge(&mut self, from: &'input str, to: &'input str) { + fn name(&self) -> String { + //SAFETY: the name is interned during graph creation and stored as name_sym. + // gives much better performance on lookup. + unsafe { self.interner.resolve_unchecked(self.name_sym).to_owned() } + } + fn get_node_name(&self, node_sym: Sym) -> &str { + //SAFETY: the only way to get a Sym is by manipulating an interned string. + // gives much better performance on lookup. + + unsafe { self.interner.resolve_unchecked(node_sym) } + } + + fn add_edge(&mut self, from: Sym, to: Sym) { let from_node = self.nodes.entry(from).or_default(); if from != to { from_node.add_successor(to); @@ -185,71 +260,76 @@ impl<'input> Graph<'input> { } } - fn remove_edge(&mut self, u: &'input str, v: &'input str) { - remove(&mut self.nodes.get_mut(u).unwrap().successor_names, v); - self.nodes.get_mut(v).unwrap().predecessor_count -= 1; + fn remove_edge(&mut self, u: Sym, v: Sym) { + remove( + &mut self + .nodes + .get_mut(&u) + .expect("node is part of the graph") + .successor_tokens, + v, + ); + self.nodes + .get_mut(&v) + .expect("node is part of the graph") + .predecessor_count -= 1; } /// Implementation of algorithm T from TAOCP (Don. Knuth), vol. 1. fn run_tsort(&mut self) { - // First, we find nodes that have no prerequisites (independent nodes). - // If no such node exists, then there is a cycle. - let mut independent_nodes_queue: VecDeque<&'input str> = self + let mut independent_nodes_queue: VecDeque = self .nodes .iter() - .filter_map(|(&name, node)| { + .filter_map(|(&sym, node)| { if node.predecessor_count == 0 { - Some(name) + Some(sym) } else { None } }) .collect(); - // To make sure the resulting ordering is deterministic we - // need to order independent nodes. - // - // FIXME: this doesn't comply entirely with the GNU coreutils - // implementation. - independent_nodes_queue.make_contiguous().sort_unstable(); + // Sort by resolved string for deterministic output + independent_nodes_queue + .make_contiguous() + .sort_unstable_by(|a, b| self.get_node_name(*a).cmp(self.get_node_name(*b))); while !self.nodes.is_empty() { - // Get the next node (breaking any cycles necessary to do so). let v = self.find_next_node(&mut independent_nodes_queue); - println!("{v}"); - if let Some(node_to_process) = self.nodes.remove(v) { - for successor_name in node_to_process.successor_names.into_iter().rev() { - let successor_node = self.nodes.get_mut(successor_name).unwrap(); + println!("{}", self.get_node_name(v)); + if let Some(node_to_process) = self.nodes.remove(&v) { + for successor_name in node_to_process.successor_tokens.into_iter().rev() { + // we reverse to match GNU tsort order + let successor_node = self + .nodes + .get_mut(&successor_name) + .expect("node is part of the graph"); successor_node.predecessor_count -= 1; if successor_node.predecessor_count == 0 { - // If we find nodes without any other prerequisites, we add them to the queue. independent_nodes_queue.push_back(successor_name); } } } } } - - /// Get the in-degree of the node with the given name. - fn indegree(&self, name: &str) -> Option { - self.nodes.get(name).map(|data| data.predecessor_count) + pub fn indegree(&self, sym: Sym) -> Option { + self.nodes.get(&sym).map(|data| data.predecessor_count) } - // Pre-condition: self.nodes is non-empty. - fn find_next_node(&mut self, frontier: &mut VecDeque<&'input str>) -> &'input str { + fn find_next_node(&mut self, frontier: &mut VecDeque) -> Sym { // If there are no nodes of in-degree zero but there are still // un-visited nodes in the graph, then there must be a cycle. - // We need to find the cycle, display it, and then break the - // cycle. + // We need to find the cycle, display it on stderr, and break it to go on. // // A cycle is guaranteed to be of length at least two. We break // the cycle by deleting an arbitrary edge (the first). That is // not necessarily the optimal thing, but it should be enough to - // continue making progress in the graph traversal. + // continue making progress in the graph traversal, and matches GNU tsort behavior. // // It is possible that deleting the edge does not actually // result in the target node having in-degree zero, so we repeat // the process until such a node appears. + loop { match frontier.pop_front() { None => self.find_and_break_cycle(frontier), @@ -258,27 +338,28 @@ impl<'input> Graph<'input> { } } - fn find_and_break_cycle(&mut self, frontier: &mut VecDeque<&'input str>) { + fn find_and_break_cycle(&mut self, frontier: &mut VecDeque) { let cycle = self.detect_cycle(); - show!(TsortError::Loop(self.name.clone())); - for &node in &cycle { - show!(LoopNode(node)); + show!(TsortError::Loop(self.name())); + for &sym in &cycle { + show!(LoopNode(self.get_node_name(sym))); } let u = *cycle.last().expect("cycle must be non-empty"); let v = cycle[0]; self.remove_edge(u, v); - if self.indegree(v).unwrap() == 0 { + if self.indegree(v).expect("node is part of the graph") == 0 { frontier.push_back(v); } } - fn detect_cycle(&self) -> Vec<&'input str> { - let mut nodes: Vec<_> = self.nodes.keys().collect(); - nodes.sort_unstable(); + fn detect_cycle(&self) -> Vec { + // Sort by resolved string for deterministic output + let mut nodes: Vec<_> = self.nodes.keys().copied().collect(); + nodes.sort_unstable_by(|a, b| self.get_node_name(*a).cmp(self.get_node_name(*b))); let mut visited = HashMap::new(); let mut stack = Vec::with_capacity(self.nodes.len()); - for node in nodes { + for &node in &nodes { if self.dfs(node, &mut visited, &mut stack) { let (loop_entry, _) = stack.pop().expect("loop is not empty"); @@ -294,13 +375,15 @@ impl<'input> Graph<'input> { fn dfs<'a>( &'a self, - node: &'input str, - visited: &mut HashMap<&'input str, VisitedState>, - stack: &mut Vec<(&'input str, &'a [&'input str])>, + node: Sym, + visited: &mut HashMap, + stack: &mut Vec<(Sym, &'a [Sym])>, ) -> bool { stack.push(( node, - self.nodes.get(node).map_or(&[], |n| &n.successor_names), + self.nodes + .get(&node) + .map_or(&[], |n: &Node| &n.successor_tokens), )); let state = *visited.entry(node).or_insert(VisitedState::Opened); @@ -320,22 +403,19 @@ impl<'input> Graph<'input> { match visited.entry(next_node) { Entry::Vacant(v) => { - // It's a first time we enter this node + // first visit of the node v.insert(VisitedState::Opened); stack.push(( next_node, self.nodes - .get(next_node) - .map_or(&[], |n| &n.successor_names), + .get(&next_node) + .map_or(&[], |n| &n.successor_tokens), )); } Entry::Occupied(o) => { if *o.get() == VisitedState::Opened { - // we are entering the same opened node again -> loop found - // stack contains it - // - // But part of the stack may not be belonging to this loop - // push found node to the stack to be able to trace the beginning of the loop + // We have found a node that was already visited by another iteration => loop completed + // the stack may contain unrelated nodes. This allows narrowing the loop down. stack.push((next_node, &[])); return true; } diff --git a/src/uu/unexpand/src/unexpand.rs b/src/uu/unexpand/src/unexpand.rs index b3990ac596f..896318484dd 100644 --- a/src/uu/unexpand/src/unexpand.rs +++ b/src/uu/unexpand/src/unexpand.rs @@ -13,7 +13,6 @@ use std::num::IntErrorKind; use std::path::Path; use std::str::from_utf8; use thiserror::Error; -use unicode_width::UnicodeWidthChar; use uucore::display::Quotable; use uucore::error::{FromIo, UError, UResult, USimpleError}; use uucore::translate; @@ -279,11 +278,7 @@ fn next_char_info(uflag: bool, buf: &[u8], byte: usize) -> (CharType, usize, usi Some(' ') => (CharType::Space, 0, 1), Some('\t') => (CharType::Tab, 0, 1), Some('\x08') => (CharType::Backspace, 0, 1), - Some(c) => ( - CharType::Other, - UnicodeWidthChar::width(c).unwrap_or(0), - nbytes, - ), + Some(_) => (CharType::Other, nbytes, nbytes), None => { // invalid char snuck past the utf8_validation_iterator somehow??? (CharType::Other, 1, 1) diff --git a/src/uu/uniq/src/uniq.rs b/src/uu/uniq/src/uniq.rs index 3845ba459ea..ae9b88f2d47 100644 --- a/src/uu/uniq/src/uniq.rs +++ b/src/uu/uniq/src/uniq.rs @@ -61,8 +61,6 @@ struct Uniq { struct LineMeta { key_start: usize, key_end: usize, - lowercase: Vec, - use_lowercase: bool, } macro_rules! write_line_terminator { @@ -97,7 +95,15 @@ impl Uniq { self.build_meta(&next_buf, &mut next_meta); - if self.keys_differ(¤t_buf, ¤t_meta, &next_buf, &next_meta) { + if self.keys_are_equal(¤t_buf, ¤t_meta, &next_buf, &next_meta) { + if self.all_repeated { + self.print_line(writer, ¤t_buf, group_count, first_line_printed)?; + first_line_printed = true; + std::mem::swap(&mut current_buf, &mut next_buf); + std::mem::swap(&mut current_meta, &mut next_meta); + } + group_count += 1; + } else { if (group_count == 1 && !self.repeats_only) || (group_count > 1 && !self.uniques_only) { @@ -107,14 +113,6 @@ impl Uniq { std::mem::swap(&mut current_buf, &mut next_buf); std::mem::swap(&mut current_meta, &mut next_meta); group_count = 1; - } else { - if self.all_repeated { - self.print_line(writer, ¤t_buf, group_count, first_line_printed)?; - first_line_printed = true; - std::mem::swap(&mut current_buf, &mut next_buf); - std::mem::swap(&mut current_meta, &mut next_meta); - } - group_count += 1; } next_buf.clear(); } @@ -138,7 +136,7 @@ impl Uniq { if self.zero_terminated { 0 } else { b'\n' } } - fn keys_differ( + fn keys_are_equal( &self, first_line: &[u8], first_meta: &LineMeta, @@ -148,22 +146,11 @@ impl Uniq { let first_slice = &first_line[first_meta.key_start..first_meta.key_end]; let second_slice = &second_line[second_meta.key_start..second_meta.key_end]; - if !self.ignore_case { - return first_slice != second_slice; - } - - let first_cmp = if first_meta.use_lowercase { - first_meta.lowercase.as_slice() - } else { - first_slice - }; - let second_cmp = if second_meta.use_lowercase { - second_meta.lowercase.as_slice() + if self.ignore_case { + first_slice.eq_ignore_ascii_case(second_slice) } else { - second_slice - }; - - first_cmp != second_cmp + first_slice == second_slice + } } fn key_bounds(&self, line: &[u8]) -> (usize, usize) { @@ -230,20 +217,6 @@ impl Uniq { let (key_start, key_end) = self.key_bounds(line); meta.key_start = key_start; meta.key_end = key_end; - - if self.ignore_case && key_start < key_end { - let slice = &line[key_start..key_end]; - if slice.iter().any(|b| b.is_ascii_uppercase()) { - meta.lowercase.clear(); - meta.lowercase.reserve(slice.len()); - meta.lowercase - .extend(slice.iter().map(|b| b.to_ascii_lowercase())); - meta.use_lowercase = true; - return; - } - } - - meta.use_lowercase = false; } fn read_line( diff --git a/src/uu/who/src/platform/unix.rs b/src/uu/who/src/platform/unix.rs index 8e72a83ba39..5cd27f26b92 100644 --- a/src/uu/who/src/platform/unix.rs +++ b/src/uu/who/src/platform/unix.rs @@ -195,13 +195,10 @@ fn current_tty() -> String { impl Who { #[allow(clippy::cognitive_complexity)] fn exec(&mut self) -> UResult<()> { - let run_level_chk = |_record: i16| { - #[cfg(not(target_os = "linux"))] - return false; - - #[cfg(target_os = "linux")] - return _record == utmpx::RUN_LVL; - }; + #[cfg(target_os = "linux")] + let run_level_chk = |record: i16| record == utmpx::RUN_LVL; + #[cfg(not(target_os = "linux"))] + let run_level_chk = |_| false; let f = if self.args.len() == 1 { self.args[0].as_ref() diff --git a/src/uucore/src/lib/features/benchmark.rs b/src/uucore/src/lib/features/benchmark.rs index 306ffdc3da7..8be0baf720a 100644 --- a/src/uucore/src/lib/features/benchmark.rs +++ b/src/uucore/src/lib/features/benchmark.rs @@ -289,6 +289,46 @@ pub mod text_data { } } +/// Binary data generation utilities for benchmarking +pub mod binary_data { + use std::fs::File; + use std::io::Write; + use std::path::Path; + + /// Create a binary file filled with a repeated pattern + /// + /// Creates a file of the specified size (in MB) filled with the given byte pattern. + /// This is useful for benchmarking utilities that work with large binary files like dd, cp, etc. + pub fn create_file(path: &Path, size_mb: usize, pattern: u8) { + let buffer = vec![pattern; size_mb * 1024 * 1024]; + let mut file = File::create(path).unwrap(); + file.write_all(&buffer).unwrap(); + file.sync_all().unwrap(); + } +} + +/// Filesystem utilities for benchmarking +pub mod fs_utils { + use std::fs; + use std::path::Path; + + /// Remove a file or directory if it exists + /// + /// This is a convenience function for cleaning up between benchmark iterations. + /// It handles both files and directories, and is a no-op if the path doesn't exist. + pub fn remove_path(path: &Path) { + if !path.exists() { + return; + } + + if path.is_dir() { + fs::remove_dir_all(path).unwrap(); + } else { + fs::remove_file(path).unwrap(); + } + } +} + /// Filesystem tree generation utilities for benchmarking pub mod fs_tree { use std::fs::{self, File}; diff --git a/src/uucore/src/lib/features/checksum/mod.rs b/src/uucore/src/lib/features/checksum/mod.rs index 2f3d28b4121..e272cdea68b 100644 --- a/src/uucore/src/lib/features/checksum/mod.rs +++ b/src/uucore/src/lib/features/checksum/mod.rs @@ -374,9 +374,6 @@ pub enum ChecksumError { #[error("the --raw option is not supported with multiple files")] RawMultipleFiles, - #[error("the --{0} option is meaningful only when verifying checksums")] - CheckOnlyFlag(String), - // --length sanitization errors #[error("--length required for {}", .0.quote())] LengthRequired(String), @@ -393,8 +390,6 @@ pub enum ChecksumError { #[error("--length is only supported with --algorithm blake2b, sha2, or sha3")] LengthOnlyForBlake2bSha2Sha3, - #[error("the --binary and --text options are meaningless when verifying checksums")] - BinaryTextConflict, #[error("--text mode is only supported with --untagged")] TextWithoutUntagged, #[error("--check is not supported with --algorithm={{bsd,sysv,crc,crc32b}}")] diff --git a/src/uucore/src/lib/features/fs.rs b/src/uucore/src/lib/features/fs.rs index 16de054a3c2..a783d04eace 100644 --- a/src/uucore/src/lib/features/fs.rs +++ b/src/uucore/src/lib/features/fs.rs @@ -13,6 +13,8 @@ use libc::{ S_IRUSR, S_ISGID, S_ISUID, S_ISVTX, S_IWGRP, S_IWOTH, S_IWUSR, S_IXGRP, S_IXOTH, S_IXUSR, mkfifo, mode_t, }; +#[cfg(all(unix, not(target_os = "redox")))] +pub use libc::{major, makedev, minor}; use std::collections::HashSet; use std::collections::VecDeque; use std::env; @@ -136,7 +138,6 @@ impl FileInformation { any( target_vendor = "apple", target_os = "android", - target_os = "freebsd", target_os = "netbsd", target_os = "openbsd", target_os = "illumos", @@ -150,6 +151,8 @@ impl FileInformation { ) ))] return self.0.st_nlink.into(); + #[cfg(target_os = "freebsd")] + return self.0.st_nlink; #[cfg(target_os = "aix")] return self.0.st_nlink.try_into().unwrap(); #[cfg(windows)] @@ -158,16 +161,9 @@ impl FileInformation { #[cfg(unix)] pub fn inode(&self) -> u64 { - #[cfg(all( - not(any(target_os = "freebsd", target_os = "netbsd")), - target_pointer_width = "64" - ))] + #[cfg(all(not(any(target_os = "netbsd")), target_pointer_width = "64"))] return self.0.st_ino; - #[cfg(any( - target_os = "freebsd", - target_os = "netbsd", - not(target_pointer_width = "64") - ))] + #[cfg(any(target_os = "netbsd", not(target_pointer_width = "64")))] return self.0.st_ino.into(); } } @@ -765,10 +761,13 @@ pub mod sane_blksize { /// /// If the metadata contain invalid values a meaningful adaption /// of that value is done. - pub fn sane_blksize_from_metadata(_metadata: &std::fs::Metadata) -> u64 { + pub fn sane_blksize_from_metadata( + #[cfg(unix)] metadata: &std::fs::Metadata, + #[cfg(not(unix))] _: &std::fs::Metadata, + ) -> u64 { #[cfg(not(target_os = "windows"))] { - sane_blksize(_metadata.blksize()) + sane_blksize(metadata.blksize()) } #[cfg(target_os = "windows")] @@ -839,6 +838,24 @@ pub fn make_fifo(path: &Path) -> std::io::Result<()> { } } +// Redox's libc appears not to include the following utilities + +#[cfg(target_os = "redox")] +pub fn major(dev: libc::dev_t) -> libc::c_uint { + (((dev >> 8) & 0xFFF) | ((dev >> 32) & 0xFFFFF000)) as _ +} + +#[cfg(target_os = "redox")] +pub fn minor(dev: libc::dev_t) -> libc::c_uint { + ((dev & 0xFF) | ((dev >> 12) & 0xFFFFF00)) as _ +} + +#[cfg(target_os = "redox")] +pub fn makedev(maj: libc::c_uint, min: libc::c_uint) -> libc::dev_t { + let [maj, min] = [maj as libc::dev_t, min as libc::dev_t]; + (min & 0xff) | ((maj & 0xfff) << 8) | ((min & !0xff) << 12) | ((maj & !0xfff) << 32) +} + #[cfg(test)] mod tests { // Note this useful idiom: importing names from outer (for mod tests) scope. diff --git a/src/uucore/src/lib/features/fsext.rs b/src/uucore/src/lib/features/fsext.rs index ce734ff2d32..0b6e59acb5d 100644 --- a/src/uucore/src/lib/features/fsext.rs +++ b/src/uucore/src/lib/features/fsext.rs @@ -380,7 +380,7 @@ impl From for MountInfo { } } -#[cfg(all(unix, not(any(target_os = "aix", target_os = "redox"))))] +#[cfg(all(unix, not(target_os = "redox")))] fn is_dummy_filesystem(fs_type: &str, mount_option: &str) -> bool { // spell-checker:disable match fs_type { @@ -392,7 +392,9 @@ fn is_dummy_filesystem(fs_type: &str, mount_option: &str) -> bool { // for NetBSD 3.0 | "kernfs" // for Irix 6.5 - | "ignore" => true, + | "ignore" + // Binary format support pseudo-filesystem + | "binfmt_misc" => true, _ => fs_type == "none" && !mount_option.contains(MOUNT_OPT_BIND) } @@ -418,40 +420,6 @@ fn mount_dev_id(mount_dir: &OsStr) -> String { } } -#[cfg(any( - target_os = "freebsd", - target_vendor = "apple", - target_os = "netbsd", - target_os = "openbsd" -))] -use libc::c_int; -#[cfg(any( - target_os = "freebsd", - target_vendor = "apple", - target_os = "netbsd", - target_os = "openbsd" -))] -unsafe extern "C" { - #[cfg(all(target_vendor = "apple", target_arch = "x86_64"))] - #[link_name = "getmntinfo$INODE64"] - fn get_mount_info(mount_buffer_p: *mut *mut StatFs, flags: c_int) -> c_int; - - #[cfg(any( - target_os = "netbsd", - target_os = "openbsd", - all(target_vendor = "apple", target_arch = "aarch64") - ))] - #[link_name = "getmntinfo"] - fn get_mount_info(mount_buffer_p: *mut *mut StatFs, flags: c_int) -> c_int; - - // Rust on FreeBSD uses 11.x ABI for filesystem metadata syscalls. - // Call the right version of the symbol for getmntinfo() result to - // match libc StatFS layout. - #[cfg(target_os = "freebsd")] - #[link_name = "getmntinfo@FBSD_1.0"] - fn get_mount_info(mount_buffer_p: *mut *mut StatFs, flags: c_int) -> c_int; -} - use crate::error::UResult; #[cfg(any( target_os = "freebsd", @@ -506,9 +474,9 @@ pub fn read_fs_list() -> UResult> { ))] { let mut mount_buffer_ptr: *mut StatFs = ptr::null_mut(); - let len = unsafe { get_mount_info(&raw mut mount_buffer_ptr, 1_i32) }; + let len = unsafe { libc::getmntinfo(&raw mut mount_buffer_ptr, 1_i32) }; if len < 0 { - return Err(USimpleError::new(1, "get_mount_info() failed")); + return Err(USimpleError::new(1, "getmntinfo() failed")); } let mounts = unsafe { slice::from_raw_parts(mount_buffer_ptr, len as usize) }; Ok(mounts @@ -1220,4 +1188,12 @@ mod tests { crate::os_str_from_bytes(b"/mnt/some- -dir-\xf3").unwrap() ); } + + #[test] + #[cfg(all(unix, not(target_os = "redox")))] + // spell-checker:ignore (word) binfmt + fn test_binfmt_misc_is_dummy() { + use super::is_dummy_filesystem; + assert!(is_dummy_filesystem("binfmt_misc", "")); + } } diff --git a/src/uucore/src/lib/features/parser/num_parser.rs b/src/uucore/src/lib/features/parser/num_parser.rs index 178cd578fba..b23f51fb52e 100644 --- a/src/uucore/src/lib/features/parser/num_parser.rs +++ b/src/uucore/src/lib/features/parser/num_parser.rs @@ -7,10 +7,8 @@ // spell-checker:ignore powf copysign prec ilog inity infinit infs bigdecimal extendedbigdecimal biguint underflowed muls -use std::num::NonZeroU64; - use bigdecimal::{ - BigDecimal, Context, + BigDecimal, num_bigint::{BigInt, BigUint, Sign}, }; use num_traits::Signed; @@ -398,71 +396,6 @@ fn make_error(overflow: bool, negative: bool) -> ExtendedParserError -/// -/// TODO: Still pending discussion in , -/// we do lose a little bit of precision, and the last digits may not be correct. -/// Note: This has been copied from the latest revision in , -/// so it's using minimum Rust version of `bigdecimal-rs`. -fn pow_with_context(bd: &BigDecimal, exp: i64, ctx: &Context) -> BigDecimal { - if exp == 0 { - return 1.into(); - } - - // When performing a multiplication between 2 numbers, we may lose up to 2 digits - // of precision. - // "Proof": https://github.com/akubera/bigdecimal-rs/issues/147#issuecomment-2793431202 - const MARGIN_PER_MUL: u64 = 2; - // When doing many multiplication, we still introduce additional errors, add 1 more digit - // per 10 multiplications. - const MUL_PER_MARGIN_EXTRA: u64 = 10; - - fn trim_precision(bd: BigDecimal, ctx: &Context, margin: u64) -> BigDecimal { - let prec = ctx.precision().get() + margin; - if bd.digits() > prec { - bd.with_precision_round(NonZeroU64::new(prec).unwrap(), ctx.rounding_mode()) - } else { - bd - } - } - - // Count the number of multiplications we're going to perform, one per "1" binary digit - // in exp, and the number of times we can divide exp by 2. - let mut n = exp.unsigned_abs(); - // Note: 63 - n.leading_zeros() == n.ilog2, but that's only available in recent Rust versions. - let muls = (n.count_ones() + (63 - n.leading_zeros()) - 1) as u64; - // Note: div_ceil would be nice to use here, but only available in recent Rust versions. - // (see note above about minimum Rust version in use) - let margin_extra = (muls + MUL_PER_MARGIN_EXTRA / 2) / MUL_PER_MARGIN_EXTRA; - let mut margin = margin_extra + MARGIN_PER_MUL * muls; - - let mut bd_y: BigDecimal = 1.into(); - let mut bd_x = if exp >= 0 { - bd.clone() - } else { - bd.inverse_with_context(&ctx.with_precision( - NonZeroU64::new(ctx.precision().get() + margin + MARGIN_PER_MUL).unwrap(), - )) - }; - - while n > 1 { - if n % 2 == 1 { - bd_y = trim_precision(&bd_x * bd_y, ctx, margin); - margin -= MARGIN_PER_MUL; - n -= 1; - } - bd_x = trim_precision(bd_x.square(), ctx, margin); - margin -= MARGIN_PER_MUL; - n /= 2; - } - debug_assert_eq!(margin, margin_extra); - - trim_precision(bd_x * bd_y, ctx, 0) -} - /// Construct an [`ExtendedBigDecimal`] based on parsed data fn construct_extended_big_decimal( digits: BigUint, @@ -510,7 +443,7 @@ fn construct_extended_big_decimal( let bd = BigDecimal::from_bigint(signed_digits, 0) / BigDecimal::from_bigint(BigInt::from(16).pow(scale as u32), 0); - // pow_with_context "only" supports i64 values. Just overflow/underflow if the value provided + // powi "only" supports i64 values. Just overflow/underflow if the value provided // is > 2**64 or < 2**-64. let Some(exponent) = exponent.to_i64() else { return Err(make_error(exponent.is_positive(), negative)); @@ -520,7 +453,7 @@ fn construct_extended_big_decimal( let base: BigDecimal = 2.into(); // Note: We cannot overflow/underflow BigDecimal here, as we will not be able to reach the // maximum/minimum scale (i64 range). - let pow2 = pow_with_context(&base, exponent, &Context::default()); + let pow2 = base.powi(exponent); bd * pow2 } else { diff --git a/src/uucore/src/lib/features/sum.rs b/src/uucore/src/lib/features/sum.rs index 66fb752abaf..6d190edbebd 100644 --- a/src/uucore/src/lib/features/sum.rs +++ b/src/uucore/src/lib/features/sum.rs @@ -284,8 +284,8 @@ impl Digest for Bsd { } fn result(&mut self) -> DigestOutput { - let mut _out = [0; 2]; - self.hash_finalize(&mut _out); + let mut out = [0; 2]; + self.hash_finalize(&mut out); DigestOutput::U16(self.state) } @@ -319,8 +319,8 @@ impl Digest for SysV { } fn result(&mut self) -> DigestOutput { - let mut _out = [0; 2]; - self.hash_finalize(&mut _out); + let mut out = [0; 2]; + self.hash_finalize(&mut out); DigestOutput::U16((self.state & (u16::MAX as u32)) as u16) } diff --git a/src/uucore/src/lib/features/uptime.rs b/src/uucore/src/lib/features/uptime.rs index 7e919b1ad75..91352f1de0d 100644 --- a/src/uucore/src/lib/features/uptime.rs +++ b/src/uucore/src/lib/features/uptime.rs @@ -338,13 +338,8 @@ pub fn get_nusers() -> usize { continue; } - let username = if !buffer.is_null() { - let cstr = std::ffi::CStr::from_ptr(buffer as *const i8); - cstr.to_string_lossy().to_string() - } else { - String::new() - }; - if !username.is_empty() { + let cstr = std::ffi::CStr::from_ptr(buffer.cast()); + if !cstr.is_empty() { num_user += 1; } @@ -426,11 +421,13 @@ pub fn get_loadavg() -> UResult<(f64, f64, f64)> { #[inline] pub fn get_formatted_loadavg() -> UResult { let loadavg = get_loadavg()?; - Ok(translate!( + let mut args = fluent::FluentArgs::new(); + args.set("avg1", format!("{:.2}", loadavg.0)); + args.set("avg5", format!("{:.2}", loadavg.1)); + args.set("avg15", format!("{:.2}", loadavg.2)); + Ok(crate::locale::get_message_with_args( "uptime-lib-format-loadavg", - "avg1" => format!("{:.2}", loadavg.0), - "avg5" => format!("{:.2}", loadavg.1), - "avg15" => format!("{:.2}", loadavg.2), + args, )) } @@ -501,8 +498,7 @@ mod tests { // (This is just a sanity check) assert!( uptime < 365 * 86400, - "Uptime seems unreasonably high: {} seconds", - uptime + "Uptime seems unreasonably high: {uptime} seconds" ); } @@ -518,9 +514,7 @@ mod tests { let diff = (uptime1 - uptime2).abs(); assert!( diff <= 1, - "Consecutive uptime calls should be consistent, got {} and {}", - uptime1, - uptime2 + "Consecutive uptime calls should be consistent, got {uptime1} and {uptime2}" ); } } diff --git a/src/uucore/src/lib/mods/error.rs b/src/uucore/src/lib/mods/error.rs index 0b88e389b65..ef270546cd7 100644 --- a/src/uucore/src/lib/mods/error.rs +++ b/src/uucore/src/lib/mods/error.rs @@ -748,7 +748,9 @@ impl Error for ClapErrorWrapper {} // This is abuse of the Display trait impl Display for ClapErrorWrapper { fn fmt(&self, _f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { - self.error.print().unwrap(); + // Intentionally ignore the result - error.print() writes directly to stderr + // and we always return Ok(()) to satisfy Display's contract + let _ = self.error.print(); Ok(()) } } diff --git a/tests/by-util/test_cat.rs b/tests/by-util/test_cat.rs index 33796f3ae41..2d35a2e2583 100644 --- a/tests/by-util/test_cat.rs +++ b/tests/by-util/test_cat.rs @@ -833,6 +833,37 @@ fn test_child_when_pipe_in() { ts.ucmd().pipe_in("content").run().stdout_is("content"); } +/// Regression test for GitHub issue #9769 +/// https://github.com/uutils/coreutils/issues/9769 +/// +/// Bug: Utilities panic when output is redirected to /dev/full +/// Location: src/uucore/src/lib/mods/error.rs:751 - `.unwrap()` causes panic +/// +/// This test verifies that cat handles write errors to /dev/full gracefully +/// instead of panicking with exit code 134 (SIGABRT). +/// +/// Expected behavior with current BUGGY code: +/// - Test WILL FAIL (cat panics with exit code 134) +/// +/// Expected behavior after fix: +/// - Test SHOULD PASS (cat exits gracefully with error code 1) +// Regression test for issue #9769: graceful error handling when writing to /dev/full +#[test] +#[cfg(target_os = "linux")] +fn test_write_error_handling() { + use std::fs::File; + + let dev_full = + File::create("/dev/full").expect("Failed to open /dev/full - test must run on Linux"); + + new_ucmd!() + .pipe_in("test content that should cause write error to /dev/full") + .set_stdout(dev_full) + .fails() + .code_is(1) + .stderr_contains("No space left on device"); +} + #[test] fn test_cat_eintr_handling() { // Test that cat properly handles EINTR (ErrorKind::Interrupted) during I/O operations diff --git a/tests/by-util/test_chmod.rs b/tests/by-util/test_chmod.rs index 446cdd6d39e..a17fc4a2ca3 100644 --- a/tests/by-util/test_chmod.rs +++ b/tests/by-util/test_chmod.rs @@ -375,6 +375,48 @@ fn test_permission_denied() { .stderr_is("chmod: cannot access 'd/no-x/y': Permission denied\n"); } +#[test] +#[allow(clippy::unreadable_literal)] +fn test_chmod_recursive_correct_exit_code() { + let (at, mut ucmd) = at_and_ucmd!(); + + // create 3 folders to test on + at.mkdir("a"); + at.mkdir("a/b"); + at.mkdir("z"); + + // remove read permissions for folder a so the chmod command for a/b fails + let mut perms = at.metadata("a").permissions(); + perms.set_mode(0o000); + set_permissions(at.plus_as_string("a"), perms).unwrap(); + + #[cfg(not(target_os = "linux"))] + let err_msg = "chmod: Permission denied\n"; + #[cfg(target_os = "linux")] + let err_msg = "chmod: cannot access 'a': Permission denied\n"; + + // order of command is a, a/b then c + // command is expected to fail and not just take the last exit code + ucmd.arg("-R") + .arg("--verbose") + .arg("a+w") + .arg("a") + .arg("z") + .umask(0) + .fails() + .stderr_is(err_msg); +} + +#[test] +fn test_chmod_hyper_recursive_directory_tree_does_not_fail() { + let (at, mut ucmd) = at_and_ucmd!(); + let mkdir = "a/".repeat(400); + + at.mkdir_all(&mkdir); + + ucmd.arg("-R").arg("777").arg("a").succeeds(); +} + #[test] #[allow(clippy::unreadable_literal)] fn test_chmod_recursive() { @@ -466,6 +508,17 @@ fn test_chmod_preserve_root() { .stderr_contains("chmod: it is dangerous to operate recursively on '/'"); } +#[test] +fn test_chmod_preserve_root_with_paths_that_resolve_to_root() { + new_ucmd!() + .arg("-R") + .arg("--preserve-root") + .arg("755") + .arg("/../") + .fails_with_code(1) + .stderr_contains("chmod: it is dangerous to operate recursively on '/'"); +} + #[test] fn test_chmod_symlink_non_existing_file() { let scene = TestScenario::new(util_name!()); diff --git a/tests/by-util/test_cksum.rs b/tests/by-util/test_cksum.rs index d4685d6198f..40f49fc70f8 100644 --- a/tests/by-util/test_cksum.rs +++ b/tests/by-util/test_cksum.rs @@ -1066,7 +1066,7 @@ mod output_format { .args(&["-a", "md5"]) .arg(at.subdir.join("f")) .fails_with_code(1) - .stderr_contains("--text mode is only supported with --untagged"); + .stderr_contains("the following required arguments were not provided"); //clap does not change the meaning } #[test] @@ -1216,7 +1216,7 @@ fn test_conflicting_options() { .fails_with_code(1) .no_stdout() .stderr_contains( - "cksum: the --binary and --text options are meaningless when verifying checksums", + "cannot be used with", //clap generated error ); scene @@ -1228,7 +1228,7 @@ fn test_conflicting_options() { .fails_with_code(1) .no_stdout() .stderr_contains( - "cksum: the --binary and --text options are meaningless when verifying checksums", + "cannot be used with", //clap generated error ); } diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index 2563e533ae4..dfd07ec4bd0 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -7400,3 +7400,101 @@ fn test_cp_recurse_verbose_output_with_symlink_already_exists() { .no_stderr() .stdout_is(output); } + +#[test] +#[cfg(unix)] +fn test_cp_hlp_flag_ordering() { + // GNU cp: "If more than one of -H, -L, and -P is specified, only the final one takes effect" + let (at, mut ucmd) = at_and_ucmd!(); + at.touch("file.txt"); + at.symlink_file("file.txt", "symlink"); + + // -HP: P wins, copy symlink as symlink + ucmd.args(&["-HP", "symlink", "dest_hp"]).succeeds(); + assert!(at.is_symlink("dest_hp")); + + // -PH: H wins, copy target file + let (at, mut ucmd) = at_and_ucmd!(); + at.touch("file.txt"); + at.symlink_file("file.txt", "symlink"); + ucmd.args(&["-PH", "symlink", "dest_ph"]).succeeds(); + assert!(!at.is_symlink("dest_ph")); + assert!(at.file_exists("dest_ph")); +} + +#[test] +#[cfg(unix)] +fn test_cp_archive_deref_flag_ordering() { + // (flags, expect_symlink): last flag wins; a/d imply -P, H/L dereference + for (flags, expect_symlink) in [ + ("-Ha", true), + ("-aH", false), + ("-Hd", true), + ("-dH", false), + ("-La", true), + ("-aL", false), + ("-Ld", true), + ("-dL", false), + ] { + let (at, mut ucmd) = at_and_ucmd!(); + at.touch("file.txt"); + at.symlink_file("file.txt", "symlink"); + let dest = format!("dest{flags}"); + ucmd.args(&[flags, "symlink", &dest]).succeeds(); + assert_eq!(at.is_symlink(&dest), expect_symlink, "failed for {flags}"); + } +} + +#[test] +fn test_cp_circular_symbolic_links_in_directory() { + let source_dir = "source_dir"; + let target_dir = "target_dir"; + let (at, mut ucmd) = at_and_ucmd!(); + let separator = std::path::MAIN_SEPARATOR_STR; + + at.mkdir(source_dir); + at.symlink_file( + format!("{source_dir}/a").as_str(), + format!("{source_dir}/b").as_str(), + ); + at.symlink_file( + format!("{source_dir}/b").as_str(), + format!("{source_dir}/a").as_str(), + ); + + ucmd.arg(source_dir) + .arg(target_dir) + .arg("-rL") + .fails_with_code(1) + .stderr_contains(format!( + "IO error for operation on {source_dir}{separator}a" + )) + .stderr_contains(format!( + "IO error for operation on {source_dir}{separator}b" + )); +} + +/// Test that copying to an existing file maintains its permissions, unix only because .mode() only +/// works on Unix +#[test] +#[cfg(unix)] +fn test_cp_to_existing_file_permissions() { + let (at, mut ucmd) = at_and_ucmd!(); + + at.touch("src"); + at.touch("dst"); + + let src_path = at.plus("src"); + let dst_path = at.plus("dst"); + + let mut src_permissions = std::fs::metadata(&src_path).unwrap().permissions(); + src_permissions.set_readonly(true); + std::fs::set_permissions(&src_path, src_permissions).unwrap(); + + let dst_mode = std::fs::metadata(&dst_path).unwrap().permissions().mode(); + + ucmd.args(&["src", "dst"]).succeeds(); + + let new_dst_mode = std::fs::metadata(&dst_path).unwrap().permissions().mode(); + assert_eq!(dst_mode, new_dst_mode); +} diff --git a/tests/by-util/test_csplit.rs b/tests/by-util/test_csplit.rs index bf46063109a..76c217a29bb 100644 --- a/tests/by-util/test_csplit.rs +++ b/tests/by-util/test_csplit.rs @@ -1551,3 +1551,35 @@ fn test_csplit_non_utf8_paths() { ucmd.arg(&filename).arg("3").succeeds(); } + +/// Test write error detection using /dev/full +#[test] +#[cfg(target_os = "linux")] +fn test_write_error_dev_full() { + let (at, mut ucmd) = at_and_ucmd!(); + at.symlink_file("/dev/full", "xx01"); + + ucmd.args(&["-", "2"]) + .pipe_in("1\n2\n") + .fails_with_code(1) + .stderr_contains("xx01: No space left on device"); + + // Files cleaned up by default + assert!(!at.file_exists("xx00")); +} + +/// Test write error with -k keeps files +#[test] +#[cfg(target_os = "linux")] +fn test_write_error_dev_full_keep_files() { + let (at, mut ucmd) = at_and_ucmd!(); + at.symlink_file("/dev/full", "xx01"); + + ucmd.args(&["-k", "-", "2"]) + .pipe_in("1\n2\n") + .fails_with_code(1) + .stderr_contains("xx01: No space left on device"); + + assert!(at.file_exists("xx00")); + assert_eq!(at.read("xx00"), "1\n"); +} diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index 9a98b1b0309..b0613b14608 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -1132,6 +1132,46 @@ fn test_date_military_timezone_with_offset_variations() { } } +#[test] +fn test_date_military_timezone_with_offset_and_date() { + use chrono::{Duration, Utc}; + + let today = Utc::now().date_naive(); + + let test_cases = vec![ + ("m", -1), // M = UTC+12 + ("a", -1), // A = UTC+1 + ("n", 0), // N = UTC-1 + ("y", 0), // Y = UTC-12 + ("z", 0), // Z = UTC + // same day hour offsets + ("n2", 0), + // midnight crossings with hour offsets back to today + ("a1", 0), // exactly to midnight + ("a5", 0), // "overflow" midnight + ("m23", 0), + // midnight crossings with hour offsets to tomorrow + ("n23", 1), + ("y23", 1), + // midnight crossing to yesterday even with positive offset + ("m9", -1), // M = UTC+12 (-12 h + 9h is still `yesterday`) + ]; + + for (input, day_delta) in test_cases { + let expected_date = today.checked_add_signed(Duration::days(day_delta)).unwrap(); + + let expected = format!("{}\n", expected_date.format("%F")); + + new_ucmd!() + .env("TZ", "UTC") + .arg("-d") + .arg(input) + .arg("+%F") + .succeeds() + .stdout_is(expected); + } +} + // Locale-aware hour formatting tests #[test] #[cfg(unix)] diff --git a/tests/by-util/test_dd.rs b/tests/by-util/test_dd.rs index a6a52e66fb5..35a1561e498 100644 --- a/tests/by-util/test_dd.rs +++ b/tests/by-util/test_dd.rs @@ -2,7 +2,7 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore fname, tname, fpath, specfile, testfile, unspec, ifile, ofile, outfile, fullblock, urand, fileio, atoe, atoibm, availible, behaviour, bmax, bremain, btotal, cflags, creat, ctable, ctty, datastructures, doesnt, etoa, fileout, fname, gnudd, iconvflags, iseek, nocache, noctty, noerror, nofollow, nolinks, nonblock, oconvflags, oseek, outfile, parseargs, rlen, rmax, rposition, rremain, rsofar, rstat, sigusr, sigval, wlen, wstat abcdefghijklm abcdefghi nabcde nabcdefg abcdefg fifoname seekable +// spell-checker:ignore fname, tname, fpath, specfile, testfile, unspec, ifile, ofile, outfile, fullblock, urand, fileio, atoe, atoibm, availible, behaviour, bmax, bremain, btotal, cflags, creat, ctable, ctty, datastructures, doesnt, etoa, fileout, fname, gnudd, iconvflags, iseek, nocache, noctty, noerror, nofollow, nolinks, nonblock, oconvflags, oseek, outfile, parseargs, rlen, rmax, rposition, rremain, rsofar, rstat, sigusr, sigval, wlen, wstat abcdefghijklm abcdefghi nabcde nabcdefg abcdefg fifoname seekable fadvise FADV DONTNEED use uutests::at_and_ucmd; use uutests::new_ucmd; @@ -1840,3 +1840,52 @@ fn test_skip_overflow() { "dd: invalid number: ‘9223372036854775808’: Value too large for defined data type", ); } + +#[test] +#[cfg(target_os = "linux")] +fn test_nocache_eof() { + let (at, mut ucmd) = at_and_ucmd!(); + at.write_bytes("in.f", &vec![0u8; 1234567]); + ucmd.args(&[ + "if=in.f", + "of=out.f", + "bs=1M", + "oflag=nocache,sync", + "status=noxfer", + ]) + .succeeds(); + assert_eq!(at.read_bytes("out.f").len(), 1234567); +} + +#[test] +#[cfg(all(target_os = "linux", feature = "printf"))] +fn test_nocache_eof_fadvise_zero_length() { + use std::process::Command; + let (at, _ucmd) = at_and_ucmd!(); + at.write_bytes("in.f", &vec![0u8; 1234567]); + + let strace_file = at.plus_as_string("strace.out"); + let result = Command::new("strace") + .args(["-o", &strace_file, "-e", "fadvise64,fadvise64_64"]) + .arg(get_tests_binary()) + .args([ + "dd", + "if=in.f", + "of=out.f", + "bs=1M", + "oflag=nocache,sync", + "status=none", + ]) + .current_dir(at.as_string()) + .output(); + + if result.is_err() { + return; // strace not available + } + + let strace = at.read("strace.out"); + assert!( + strace.contains(", 0, POSIX_FADV_DONTNEED"), + "Expected len=0 at EOF: {strace}" + ); +} diff --git a/tests/by-util/test_df.rs b/tests/by-util/test_df.rs index 8b305ce4273..4754acbfe41 100644 --- a/tests/by-util/test_df.rs +++ b/tests/by-util/test_df.rs @@ -2,7 +2,7 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore udev pcent iuse itotal iused ipcent +// spell-checker:ignore udev pcent iuse itotal iused ipcent binfmt #![allow( clippy::similar_names, clippy::cast_possible_truncation, @@ -1046,3 +1046,48 @@ fn test_nonexistent_file() { .stderr_is("df: does-not-exist: No such file or directory\n") .stdout_is("File\n.\n"); } + +#[test] +#[cfg(target_os = "linux")] +fn test_df_all_shows_binfmt_misc() { + // Check if binfmt_misc is mounted + let is_mounted = std::fs::read_to_string("/proc/self/mountinfo") + .map(|content| content.lines().any(|line| line.contains("binfmt_misc"))) + .unwrap_or(false); + + if is_mounted { + let output = new_ucmd!() + .args(&["--all", "--output=fstype,target"]) + .succeeds() + .stdout_str_lossy(); + + assert!( + output.contains("binfmt_misc"), + "Expected binfmt_misc filesystem to appear in df --all output when it's mounted" + ); + } + // If binfmt_misc is not mounted, skip the test silently +} + +#[test] +#[cfg(target_os = "linux")] +fn test_df_hides_binfmt_misc_by_default() { + // Check if binfmt_misc is mounted + let is_mounted = std::fs::read_to_string("/proc/self/mountinfo") + .map(|content| content.lines().any(|line| line.contains("binfmt_misc"))) + .unwrap_or(false); + + if is_mounted { + let output = new_ucmd!() + .args(&["--output=fstype,target"]) + .succeeds() + .stdout_str_lossy(); + + // binfmt_misc should NOT appear in the output without --all + assert!( + !output.contains("binfmt_misc"), + "Expected binfmt_misc filesystem to be hidden in df output without --all" + ); + } + // If binfmt_misc is not mounted, skip the test silently +} diff --git a/tests/by-util/test_du.rs b/tests/by-util/test_du.rs index 01c612488c5..38d64d5b8b8 100644 --- a/tests/by-util/test_du.rs +++ b/tests/by-util/test_du.rs @@ -804,6 +804,44 @@ fn test_du_inodes_with_count_links_all() { assert_eq!(result_seq, ["1\td/d", "1\td/f", "1\td/h", "4\td"]); } +#[cfg(not(target_os = "android"))] +#[test] +fn test_du_count_links_hardlinks_separately() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + + at.mkdir("dir"); + at.touch("dir/file"); + at.hard_link("dir/file", "dir/hard_link"); + + let result_without_l = ts.ucmd().arg("-b").arg("dir").succeeds(); + let size_without_l: u64 = result_without_l + .stdout_str() + .split('\t') + .next() + .unwrap() + .trim() + .parse() + .unwrap(); + + for arg in ["-l", "--count-links"] { + let result_with_l = ts.ucmd().arg("-b").arg(arg).arg("dir").succeeds(); + let size_with_l: u64 = result_with_l + .stdout_str() + .split('\t') + .next() + .unwrap() + .trim() + .parse() + .unwrap(); + + assert!( + size_with_l >= size_without_l, + "With {arg}, size ({size_with_l}) should be >= size without -l ({size_without_l})" + ); + } +} + #[test] fn test_du_h_flag_empty_file() { new_ucmd!() diff --git a/tests/by-util/test_hashsum.rs b/tests/by-util/test_hashsum.rs index 2f1719b0eca..891cb9d4dfd 100644 --- a/tests/by-util/test_hashsum.rs +++ b/tests/by-util/test_hashsum.rs @@ -268,7 +268,7 @@ fn test_check_md5_ignore_missing() { .arg("--ignore-missing") .arg(at.subdir.join("testf.sha1")) .fails() - .stderr_contains("the --ignore-missing option is meaningful only when verifying checksums"); + .stderr_contains("the following required arguments were not provided"); //clap generated error } #[test] @@ -1021,13 +1021,13 @@ fn test_check_quiet() { .arg("--quiet") .arg(at.subdir.join("in.md5")) .fails() - .stderr_contains("md5sum: the --quiet option is meaningful only when verifying checksums"); + .stderr_contains("the following required arguments were not provided"); //clap generated error scene .ccmd("md5sum") .arg("--strict") .arg(at.subdir.join("in.md5")) .fails() - .stderr_contains("md5sum: the --strict option is meaningful only when verifying checksums"); + .stderr_contains("the following required arguments were not provided"); //clap generated error } #[test] diff --git a/tests/by-util/test_install.rs b/tests/by-util/test_install.rs index b6a998a02aa..e2c039222a3 100644 --- a/tests/by-util/test_install.rs +++ b/tests/by-util/test_install.rs @@ -119,6 +119,30 @@ fn test_install_ancestors_mode_directories() { assert_eq!(0o40_200_u32, at.metadata(target_dir).permissions().mode()); } +#[test] +fn test_install_remove_impermissible_dst_file() { + let src_file = "/dev/null"; + let dst_file = "/dev/full"; + new_ucmd!() + .args(&[src_file, dst_file]) + .fails() + .stderr_only(format!( + "install: failed to remove existing file '{dst_file}': Permission denied\n" + )); +} + +#[test] +fn test_install_remove_inaccessible_dst_file() { + let src_file = "/dev/null"; + let dst_file = "/root/file"; + new_ucmd!() + .args(&[src_file, dst_file]) + .fails() + .stderr_only(format!( + "install: cannot stat '{dst_file}': Permission denied\n" + )); +} + #[test] fn test_install_ancestors_mode_directories_with_file() { let (at, mut ucmd) = at_and_ucmd!(); diff --git a/tests/by-util/test_more.rs b/tests/by-util/test_more.rs index 2bf130a1858..b5256af6174 100644 --- a/tests/by-util/test_more.rs +++ b/tests/by-util/test_more.rs @@ -35,7 +35,7 @@ fn run_more_with_pty( .arg(file) .run_no_wait(); - child.delay(100); + child.delay(200); let mut output = vec![0u8; 1024]; let n = read(&controller, &mut output).unwrap(); let output_str = String::from_utf8_lossy(&output[..n]).to_string(); diff --git a/tests/by-util/test_mv.rs b/tests/by-util/test_mv.rs index 7e22d930b49..3c69d65a78d 100644 --- a/tests/by-util/test_mv.rs +++ b/tests/by-util/test_mv.rs @@ -623,6 +623,58 @@ fn test_mv_symlink_into_target() { ucmd.arg("dir-link").arg("dir").succeeds(); } +#[cfg(all(unix, not(target_os = "android")))] +#[ignore = "requires sudo"] +#[test] +fn test_mv_broken_symlink_to_another_fs() { + let scene = TestScenario::new(util_name!()); + + scene.fixtures.mkdir("foo"); + + let output = scene + .cmd("sudo") + .env("PATH", env!("PATH")) + .args(&["-E", "--non-interactive", "ls"]) + .run(); + println!("test output: {output:?}"); + + let mount = scene + .cmd("sudo") + .env("PATH", env!("PATH")) + .args(&[ + "-E", + "--non-interactive", + "mount", + "none", + "-t", + "tmpfs", + "foo", + ]) + .run(); + + if !mount.succeeded() { + print!("Test skipped; requires root user"); + return; + } + + scene.fixtures.mkdir("bar"); + scene.fixtures.symlink_file("nonexistent", "bar/baz"); + + scene + .ucmd() + .arg("bar") + .arg("foo") + .succeeds() + .no_stderr() + .no_stdout(); + + scene + .cmd("sudo") + .env("PATH", env!("PATH")) + .args(&["-E", "--non-interactive", "umount", "foo"]) + .succeeds(); +} + #[test] #[cfg(all(unix, not(target_os = "android")))] fn test_mv_hardlink_to_symlink() { diff --git a/tests/by-util/test_pr.rs b/tests/by-util/test_pr.rs index 1fa91dab2e9..63063a7e732 100644 --- a/tests/by-util/test_pr.rs +++ b/tests/by-util/test_pr.rs @@ -5,6 +5,7 @@ // spell-checker:ignore (ToDO) Sdivide use chrono::{DateTime, Duration, Utc}; +use regex::Regex; use std::fs::metadata; use uutests::new_ucmd; use uutests::util::UCommand; @@ -78,21 +79,22 @@ fn test_with_numbering_option_with_number_width() { #[test] fn test_with_long_header_option() { - let test_file_path = "test_one_page.log"; - let expected_test_file_path = "test_one_page_header.log.expected"; - let header = "new file"; - for args in [&["-h", header][..], &["--header=new file"][..]] { - let mut scenario = new_ucmd!(); - let value = file_last_modified_time(&scenario, test_file_path); - scenario - .args(args) - .arg(test_file_path) - .succeeds() - .stdout_is_templated_fixture( - expected_test_file_path, - &[("{last_modified_time}", &value), ("{header}", header)], - ); - } + let whitespace = " ".repeat(21); + let blank_lines = "\n".repeat(61); + let datetime_pattern = r"\d\d\d\d-\d\d-\d\d \d\d:\d\d"; + let pattern = + format!("\n\n{datetime_pattern}{whitespace}new file{whitespace}Page 1\n\n\na{blank_lines}"); + let regex = Regex::new(&pattern).unwrap(); + new_ucmd!() + .args(&["-h", "new file"]) + .pipe_in("a") + .succeeds() + .stdout_matches(®ex); + new_ucmd!() + .args(&["--header=new file"]) + .pipe_in("a") + .succeeds() + .stdout_matches(®ex); } #[test] @@ -400,99 +402,92 @@ fn test_with_offset_space_option() { #[test] fn test_with_date_format() { - let test_file_path = "test_one_page.log"; - let expected_test_file_path = "test_one_page.log.expected"; - let mut scenario = new_ucmd!(); - let value = file_last_modified_time_format(&scenario, test_file_path, "%Y__%s"); - scenario - .args(&[test_file_path, "-D", "%Y__%s"]) + let whitespace = " ".repeat(50); + let blank_lines = "\n".repeat(61); + let datetime_pattern = r"\d{4}__\d{10}"; + let pattern = format!("\n\n{datetime_pattern}{whitespace}Page 1\n\n\na{blank_lines}"); + let regex = Regex::new(&pattern).unwrap(); + new_ucmd!() + .args(&["-D", "%Y__%s"]) + .pipe_in("a") .succeeds() - .stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]); + .stdout_matches(®ex); // "Format" doesn't need to contain any replaceable token. + let whitespace = " ".repeat(60); + let blank_lines = "\n".repeat(61); new_ucmd!() - .args(&[test_file_path, "-D", "Hello!"]) + .args(&["-D", "Hello!"]) + .pipe_in("a") .succeeds() - .stdout_is_templated_fixture( - expected_test_file_path, - &[("{last_modified_time}", "Hello!")], - ); + .stdout_only(format!("\n\nHello!{whitespace}Page 1\n\n\na{blank_lines}")); // Long option also works new_ucmd!() - .args(&[test_file_path, "--date-format=Hello!"]) + .args(&["--date-format=Hello!"]) + .pipe_in("a") .succeeds() - .stdout_is_templated_fixture( - expected_test_file_path, - &[("{last_modified_time}", "Hello!")], - ); + .stdout_only(format!("\n\nHello!{whitespace}Page 1\n\n\na{blank_lines}")); // Option takes precedence over environment variables new_ucmd!() .env("POSIXLY_CORRECT", "1") .env("LC_TIME", "POSIX") - .args(&[test_file_path, "-D", "Hello!"]) + .args(&["--date-format=Hello!"]) + .pipe_in("a") .succeeds() - .stdout_is_templated_fixture( - expected_test_file_path, - &[("{last_modified_time}", "Hello!")], - ); + .stdout_only(format!("\n\nHello!{whitespace}Page 1\n\n\na{blank_lines}")); } #[test] fn test_with_date_format_env() { - const POSIXLY_FORMAT: &str = "%b %e %H:%M %Y"; - // POSIXLY_CORRECT + LC_ALL/TIME=POSIX uses "%b %e %H:%M %Y" date format - let test_file_path = "test_one_page.log"; - let expected_test_file_path = "test_one_page.log.expected"; - let mut scenario = new_ucmd!(); - let value = file_last_modified_time_format(&scenario, test_file_path, POSIXLY_FORMAT); - scenario + let whitespace = " ".repeat(49); + let blank_lines = "\n".repeat(61); + let datetime_pattern = r"[A-Z][a-z][a-z] [ \d]\d \d\d:\d\d \d{4}"; + let pattern = format!("\n\n{datetime_pattern}{whitespace}Page 1\n\n\na{blank_lines}"); + let regex = Regex::new(&pattern).unwrap(); + new_ucmd!() .env("POSIXLY_CORRECT", "1") .env("LC_ALL", "POSIX") - .args(&[test_file_path]) + .pipe_in("a") .succeeds() - .stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]); - - let mut scenario = new_ucmd!(); - let value = file_last_modified_time_format(&scenario, test_file_path, POSIXLY_FORMAT); - scenario + .stdout_matches(®ex); + new_ucmd!() .env("POSIXLY_CORRECT", "1") .env("LC_TIME", "POSIX") - .args(&[test_file_path]) + .pipe_in("a") .succeeds() - .stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]); + .stdout_matches(®ex); // But not if POSIXLY_CORRECT/LC_ALL is something else. - let mut scenario = new_ucmd!(); - let value = file_last_modified_time_format(&scenario, test_file_path, DATE_TIME_FORMAT_DEFAULT); - scenario + let whitespace = " ".repeat(50); + let datetime_pattern = r"\d\d\d\d-\d\d-\d\d \d\d:\d\d"; + let pattern = format!("\n\n{datetime_pattern}{whitespace}Page 1\n\n\na{blank_lines}"); + let regex = Regex::new(&pattern).unwrap(); + new_ucmd!() .env("LC_TIME", "POSIX") - .args(&[test_file_path]) + .pipe_in("a") .succeeds() - .stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]); - - let mut scenario = new_ucmd!(); - let value = file_last_modified_time_format(&scenario, test_file_path, DATE_TIME_FORMAT_DEFAULT); - scenario + .stdout_matches(®ex); + new_ucmd!() .env("POSIXLY_CORRECT", "1") .env("LC_TIME", "C") - .args(&[test_file_path]) + .pipe_in("a") .succeeds() - .stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]); + .stdout_matches(®ex); } #[test] fn test_with_pr_core_utils_tests() { let test_cases = vec![ ("", vec!["0Ft"], vec!["0F"], 0), - ("", vec!["0Fnt"], vec!["0F"], 0), + ("", vec!["0Fnt"], vec!["0Fnt-expected"], 0), ("+3", vec!["0Ft"], vec!["3-0F"], 0), ("+3 -f", vec!["0Ft"], vec!["3f-0F"], 0), ("-a -3", vec!["0Ft"], vec!["a3-0F"], 0), ("-a -3 -f", vec!["0Ft"], vec!["a3f-0F"], 0), - ("-a -3 -f", vec!["0Fnt"], vec!["a3f-0F"], 0), + ("-a -3 -f", vec!["0Fnt"], vec!["a3f-0Fnt-expected"], 0), ("+3 -a -3 -f", vec!["0Ft"], vec!["3a3f-0F"], 0), ("-l 24", vec!["FnFn"], vec!["l24-FF"], 0), ("-W 20 -l24 -f", vec!["tFFt-ll"], vec!["W20l24f-ll"], 0), @@ -610,3 +605,25 @@ fn test_help() { fn test_version() { new_ucmd!().arg("--version").succeeds(); } + +#[cfg(unix)] +#[test] +fn test_pr_char_device_dev_null() { + new_ucmd!().arg("/dev/null").succeeds(); +} + +#[test] +fn test_b_flag_backwards_compat() { + // -b is a no-op for backwards compatibility (column-down is now the default) + new_ucmd!().args(&["-b", "-t"]).pipe_in("a\nb\n").succeeds(); +} + +#[test] +fn test_page_header_width() { + let whitespace = " ".repeat(50); + let blank_lines = "\n".repeat(61); + let datetime_pattern = r"\d\d\d\d-\d\d-\d\d \d\d:\d\d"; + let pattern = format!("\n\n{datetime_pattern}{whitespace}Page 1\n\n\na{blank_lines}"); + let regex = Regex::new(&pattern).unwrap(); + new_ucmd!().pipe_in("a").succeeds().stdout_matches(®ex); +} diff --git a/tests/by-util/test_rmdir.rs b/tests/by-util/test_rmdir.rs index 0c52a22878e..669884488a0 100644 --- a/tests/by-util/test_rmdir.rs +++ b/tests/by-util/test_rmdir.rs @@ -243,3 +243,18 @@ fn test_rmdir_remove_symlink_dangling() { .fails() .stderr_is("rmdir: failed to remove 'dl/': Symbolic link not followed\n"); } + +#[cfg(any(target_os = "linux", target_os = "android"))] +#[test] +fn test_rmdir_remove_symlink_dir_with_trailing_slashes() { + // a symlink with trailing slashes should still be printing the 'Symbolic link not followed' + // message + let (at, mut ucmd) = at_and_ucmd!(); + + at.mkdir("dir"); + at.symlink_dir("dir", "dl"); + + ucmd.arg("dl////") + .fails() + .stderr_is("rmdir: failed to remove 'dl////': Symbolic link not followed\n"); +} diff --git a/tests/by-util/test_split.rs b/tests/by-util/test_split.rs index f710e14425b..497559aca3e 100644 --- a/tests/by-util/test_split.rs +++ b/tests/by-util/test_split.rs @@ -2078,3 +2078,16 @@ fn test_split_non_utf8_additional_suffix() { "Expected at least one split file to be created" ); } + +#[test] +#[cfg(target_os = "linux")] // To re-enable on Windows once I work out what goes wrong with it. +fn test_split_directory_already_exists() { + let (at, mut ucmd) = at_and_ucmd!(); + + at.mkdir("xaa"); // For collision with. + at.touch("file"); + ucmd.args(&["file"]) + .fails_with_code(1) + .no_stdout() + .stderr_is("split: xaa: Is a directory\n"); +} diff --git a/tests/by-util/test_stat.rs b/tests/by-util/test_stat.rs index 0aad7361bb8..8347d49c795 100644 --- a/tests/by-util/test_stat.rs +++ b/tests/by-util/test_stat.rs @@ -9,6 +9,9 @@ use uutests::unwrap_or_return; use uutests::util::{TestScenario, expected_result}; use uutests::util_name; +use std::fs::metadata; +use std::os::unix::fs::MetadataExt; + #[test] fn test_invalid_arg() { new_ucmd!().arg("--definitely-invalid").fails_with_code(1); @@ -567,3 +570,65 @@ fn test_mount_point_combined_with_other_specifiers() { "Should print mount point, file name, and size" ); } + +#[cfg(unix)] +#[test] +fn test_percent_escaping() { + let ts = TestScenario::new(util_name!()); + let result = ts + .ucmd() + .args(&["--printf", "%%%m%%m%m%%%", "/bin/sh"]) + .succeeds(); + assert_eq!(result.stdout_str(), "%/%m/%%"); +} + +#[cfg(unix)] +#[test] +fn test_correct_metadata() { + use uucore::fs::{major, minor}; + + let ts = TestScenario::new(util_name!()); + let parse = |(i, str): (usize, &str)| { + // Some outputs (%[fDRtT]) are in hex; they're redundant, but we might + // as well also test case conversion. + let radix = if matches!(i, 2 | 10 | 14..) { 16 } else { 10 }; + i128::from_str_radix(str, radix) + }; + for device in ["/", "/dev/null"] { + let metadata = metadata(device).unwrap(); + // We avoid time vals because of fs race conditions, especially with + // access time and status time (this previously killed an otherwise + // perfect 11-hour-long CI run...). The large number of as-casts is + // due to inconsistencies on some platforms (read: BSDs), and we use + // i128 as a lowest-common denominator. + let test_str = "%u %g %f %b %s %h %i %d %Hd %Ld %D %r %Hr %Lr %R %t %T"; + let expected = [ + metadata.uid() as _, + metadata.gid() as _, + metadata.mode() as _, + metadata.blocks() as _, + metadata.size() as _, + metadata.nlink() as _, + metadata.ino() as _, + metadata.dev() as _, + major(metadata.dev() as _) as _, + minor(metadata.dev() as _) as _, + metadata.dev() as _, + metadata.rdev() as _, + major(metadata.rdev() as _) as _, + minor(metadata.rdev() as _) as _, + metadata.rdev() as _, + major(metadata.rdev() as _) as _, + minor(metadata.rdev() as _) as _, + ]; + let result = ts.ucmd().args(&["--printf", test_str, device]).succeeds(); + let output = result + .stdout_str() + .split(' ') + .enumerate() + .map(parse) + .collect::, _>>() + .unwrap(); + assert_eq!(output, &expected); + } +} diff --git a/tests/by-util/test_stty.rs b/tests/by-util/test_stty.rs index 136ea2768af..ae64eb6aeb5 100644 --- a/tests/by-util/test_stty.rs +++ b/tests/by-util/test_stty.rs @@ -1557,6 +1557,76 @@ fn test_saved_state_with_control_chars() { .code_is(exp_result.code()); } +// Per POSIX, stty uses stdin for TTY operations. When stdin is a pipe, it should fail. +#[test] +#[cfg(unix)] +fn test_stdin_not_tty_fails() { + // ENOTTY error message varies by platform/libc: + // - glibc: "Inappropriate ioctl for device" + // - musl: "Not a tty" + // - Android: "Not a typewriter" + #[cfg(target_os = "android")] + let expected_error = "standard input: Not a typewriter"; + #[cfg(all(not(target_os = "android"), target_env = "musl"))] + let expected_error = "standard input: Not a tty"; + #[cfg(all(not(target_os = "android"), not(target_env = "musl")))] + let expected_error = "standard input: Inappropriate ioctl for device"; + + new_ucmd!() + .pipe_in("") + .fails() + .stderr_contains(expected_error); +} + +// Test that stty uses stdin for TTY operations per POSIX. +// Verifies: output redirection (#8012), save/restore pattern (#8608), stdin redirection (#8848) +#[test] +#[cfg(unix)] +fn test_stty_uses_stdin() { + use std::fs::File; + use std::process::Stdio; + + let (path, _controller, _replica) = pty_path(); + + // Output redirection: stty > file (stdin is still TTY) + let stdin = File::open(&path).unwrap(); + new_ucmd!() + .set_stdin(stdin) + .set_stdout(Stdio::piped()) + .succeeds() + .stdout_contains("speed"); + + // Save/restore: stty $(stty -g) pattern + let stdin = File::open(&path).unwrap(); + let saved = new_ucmd!() + .arg("-g") + .set_stdin(stdin) + .set_stdout(Stdio::piped()) + .succeeds() + .stdout_str() + .trim() + .to_string(); + assert!(saved.contains(':'), "Expected colon-separated saved state"); + + let stdin = File::open(&path).unwrap(); + new_ucmd!().arg(&saved).set_stdin(stdin).succeeds(); + + // Stdin redirection: stty rows 30 cols 100 < /dev/pts/N + let stdin = File::open(&path).unwrap(); + new_ucmd!() + .args(&["rows", "30", "cols", "100"]) + .set_stdin(stdin) + .succeeds(); + + let stdin = File::open(&path).unwrap(); + new_ucmd!() + .arg("--all") + .set_stdin(stdin) + .succeeds() + .stdout_contains("rows 30") + .stdout_contains("columns 100"); +} + #[test] #[cfg(unix)] fn test_columns_env_wrapping() { diff --git a/tests/by-util/test_tac.rs b/tests/by-util/test_tac.rs index 0f5aad48808..feb79f581e4 100644 --- a/tests/by-util/test_tac.rs +++ b/tests/by-util/test_tac.rs @@ -100,7 +100,7 @@ fn test_invalid_input() { .ucmd() .arg("a") .fails() - .stderr_contains("a: read error: Invalid argument"); + .stderr_contains("a: read error: Is a directory"); } #[test] diff --git a/tests/by-util/test_test.rs b/tests/by-util/test_test.rs index 4b5460cfd4a..21ea1893e99 100644 --- a/tests/by-util/test_test.rs +++ b/tests/by-util/test_test.rs @@ -1027,3 +1027,10 @@ fn test_string_lt_gt_operator() { .fails_with_code(1) .no_output(); } + +#[test] +fn test_unary_op_as_literal_in_three_arg_form() { + // `-f = a` is string comparison "-f" = "a", not file test + new_ucmd!().args(&["-f", "=", "a"]).fails_with_code(1); + new_ucmd!().args(&["-f", "=", "a", "-o", "b"]).succeeds(); +} diff --git a/tests/by-util/test_touch.rs b/tests/by-util/test_touch.rs index 68075867237..25e6b301fec 100644 --- a/tests/by-util/test_touch.rs +++ b/tests/by-util/test_touch.rs @@ -2,11 +2,12 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (formats) cymdhm cymdhms mdhm mdhms ymdhm ymdhms datetime mktime +// spell-checker:ignore (formats) cymdhm cymdhms datetime mdhm mdhms mktime strtime ymdhm ymdhms use filetime::FileTime; #[cfg(not(target_os = "freebsd"))] use filetime::set_symlink_file_times; +use jiff::{fmt::strtime, tz::TimeZone}; use std::fs::remove_file; use std::path::PathBuf; use uutests::at_and_ucmd; @@ -36,11 +37,10 @@ fn set_file_times(at: &AtPath, path: &str, atime: FileTime, mtime: FileTime) { } fn str_to_filetime(format: &str, s: &str) -> FileTime { - let tm = chrono::NaiveDateTime::parse_from_str(s, format).unwrap(); - FileTime::from_unix_time( - tm.and_utc().timestamp(), - tm.and_utc().timestamp_subsec_nanos(), - ) + let tm = strtime::parse(format, s).unwrap(); + let dt = tm.to_datetime().unwrap(); + let ts = dt.to_zoned(TimeZone::UTC).unwrap().timestamp(); + FileTime::from_unix_time(ts.as_second(), ts.subsec_nanosecond() as u32) } #[test] @@ -1052,3 +1052,10 @@ fn test_touch_non_utf8_paths() { scene.ucmd().arg(non_utf8_name).succeeds().no_output(); assert!(std::fs::metadata(at.plus(non_utf8_name)).is_ok()); } + +#[test] +#[cfg(target_os = "linux")] +fn test_touch_dev_full() { + let (_, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["/dev/full"]).succeeds().no_output(); +} diff --git a/tests/by-util/test_unexpand.rs b/tests/by-util/test_unexpand.rs index 0f2a6d464fe..0720dabb043 100644 --- a/tests/by-util/test_unexpand.rs +++ b/tests/by-util/test_unexpand.rs @@ -295,3 +295,15 @@ fn test_non_utf8_filename() { ucmd.arg(&filename).succeeds().stdout_is("\ta\n"); } + +#[test] +fn unexpand_multibyte_utf8_gnu_compat() { + // Verifies GNU-compatible behavior: column position uses byte count, not display width + // "1ΔΔΔ5" is 8 bytes (1 + 2*3 + 1), already at tab stop 8 + // So 3 spaces should NOT convert to tab (would need 8 more to reach tab stop 16) + new_ucmd!() + .args(&["-a"]) + .pipe_in("1ΔΔΔ5 99999\n") + .succeeds() + .stdout_is("1ΔΔΔ5 99999\n"); +} diff --git a/tests/fixtures/pr/0F b/tests/fixtures/pr/0F index 2237653915c..af35676eabc 100644 --- a/tests/fixtures/pr/0F +++ b/tests/fixtures/pr/0F @@ -1,6 +1,6 @@ -{last_modified_time} {file_name} Page 1 +{last_modified_time} {file_name} Page 1 @@ -66,7 +66,7 @@ -{last_modified_time} {file_name} Page 2 +{last_modified_time} {file_name} Page 2 1 FF-Test: FF's at Start of File V @@ -132,7 +132,7 @@ -{last_modified_time} {file_name} Page 3 +{last_modified_time} {file_name} Page 3 @@ -198,7 +198,7 @@ -{last_modified_time} {file_name} Page 4 +{last_modified_time} {file_name} Page 4 15 xyzxyzxyz XYZXYZXYZ abcabcab @@ -264,7 +264,7 @@ -{last_modified_time} {file_name} Page 5 +{last_modified_time} {file_name} Page 5 29 xyzxyzxyz XYZXYZXYZ abcabcab diff --git a/tests/fixtures/pr/0Fnt-expected b/tests/fixtures/pr/0Fnt-expected new file mode 100644 index 00000000000..ab2f28a0987 --- /dev/null +++ b/tests/fixtures/pr/0Fnt-expected @@ -0,0 +1,330 @@ + + +{last_modified_time} {file_name} Page 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +{last_modified_time} {file_name} Page 2 + + +1 FF-Test: FF's at Start of File V +2 Options -b -3 / -a -3 / ... +3 -------------------------------------------- +4 3456789 123456789 123456789 123456789 12345678 +5 3 Columns downwards ..., <= 5 lines per page +6 FF-Arangements: Empty Pages at start +7 \ftext; \f\ntext; +8 \f\ftext; \f\f\ntext; \f\n\ftext; \f\n\f\n; +9 3456789 123456789 123456789 +10 zzzzzzzzzzzzzzzzzzzzzzzzzz123456789 +1 12345678 +2 12345678 +3 line truncation before FF; r_r_o_l-test: +14 456789 123456789 123456789 123456789 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +{last_modified_time} {file_name} Page 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +{last_modified_time} {file_name} Page 4 + + +15 xyzxyzxyz XYZXYZXYZ abcabcab +16 456789 123456789 xyzxyzxyz XYZXYZXYZ +7 12345678 +8 12345678 +9 3456789 ab +20 DEFGHI 123 +1 12345678 +2 12345678 +3 12345678 +4 12345678 +5 12345678 +6 12345678 +27 no truncation before FF; (r_l-test): +28 no trunc + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +{last_modified_time} {file_name} Page 5 + + +29 xyzxyzxyz XYZXYZXYZ abcabcab +30 456789 123456789 xyzxyzxyz XYZXYZXYZ +1 12345678 +2 3456789 abcdefghi +3 12345678 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/fixtures/pr/3-0F b/tests/fixtures/pr/3-0F index 25a9db1719e..3a9f0b657c6 100644 --- a/tests/fixtures/pr/3-0F +++ b/tests/fixtures/pr/3-0F @@ -1,6 +1,6 @@ -{last_modified_time} {file_name} Page 3 +{last_modified_time} {file_name} Page 3 @@ -66,7 +66,7 @@ -{last_modified_time} {file_name} Page 4 +{last_modified_time} {file_name} Page 4 15 xyzxyzxyz XYZXYZXYZ abcabcab @@ -132,7 +132,7 @@ -{last_modified_time} {file_name} Page 5 +{last_modified_time} {file_name} Page 5 29 xyzxyzxyz XYZXYZXYZ abcabcab diff --git a/tests/fixtures/pr/3a3f-0F b/tests/fixtures/pr/3a3f-0F index 6097374c776..f19823acc0a 100644 --- a/tests/fixtures/pr/3a3f-0F +++ b/tests/fixtures/pr/3a3f-0F @@ -1,11 +1,11 @@ -{last_modified_time} {file_name} Page 3 +{last_modified_time} {file_name} Page 3 -{last_modified_time} {file_name} Page 4 +{last_modified_time} {file_name} Page 4 15 xyzxyzxyz XYZXYZXYZ 16 456789 123456789 xyz 7 @@ -15,7 +15,7 @@ 27 no truncation before 28 no trunc -{last_modified_time} {file_name} Page 5 +{last_modified_time} {file_name} Page 5 29 xyzxyzxyz XYZXYZXYZ 30 456789 123456789 xyz 1 diff --git a/tests/fixtures/pr/3f-0F b/tests/fixtures/pr/3f-0F index d32c1f8f653..92805024aa2 100644 --- a/tests/fixtures/pr/3f-0F +++ b/tests/fixtures/pr/3f-0F @@ -1,11 +1,11 @@ -{last_modified_time} {file_name} Page 3 +{last_modified_time} {file_name} Page 3 -{last_modified_time} {file_name} Page 4 +{last_modified_time} {file_name} Page 4 15 xyzxyzxyz XYZXYZXYZ abcabcab @@ -25,7 +25,7 @@ -{last_modified_time} {file_name} Page 5 +{last_modified_time} {file_name} Page 5 29 xyzxyzxyz XYZXYZXYZ abcabcab diff --git a/tests/fixtures/pr/a3-0F b/tests/fixtures/pr/a3-0F index 58aeb07c2e4..302ab52d109 100644 --- a/tests/fixtures/pr/a3-0F +++ b/tests/fixtures/pr/a3-0F @@ -1,6 +1,6 @@ -{last_modified_time} {file_name} Page 1 +{last_modified_time} {file_name} Page 1 @@ -66,7 +66,7 @@ -{last_modified_time} {file_name} Page 2 +{last_modified_time} {file_name} Page 2 1 FF-Test: FF's at St 2 Options -b -3 / -a 3 ------------------- @@ -132,7 +132,7 @@ -{last_modified_time} {file_name} Page 3 +{last_modified_time} {file_name} Page 3 @@ -198,7 +198,7 @@ -{last_modified_time} {file_name} Page 4 +{last_modified_time} {file_name} Page 4 15 xyzxyzxyz XYZXYZXYZ 16 456789 123456789 xyz 7 @@ -264,7 +264,7 @@ -{last_modified_time} {file_name} Page 5 +{last_modified_time} {file_name} Page 5 29 xyzxyzxyz XYZXYZXYZ 30 456789 123456789 xyz 1 diff --git a/tests/fixtures/pr/a3f-0F b/tests/fixtures/pr/a3f-0F index 24939c004b0..54e0e80b5ac 100644 --- a/tests/fixtures/pr/a3f-0F +++ b/tests/fixtures/pr/a3f-0F @@ -1,11 +1,11 @@ -{last_modified_time} {file_name} Page 1 +{last_modified_time} {file_name} Page 1 -{last_modified_time} {file_name} Page 2 +{last_modified_time} {file_name} Page 2 1 FF-Test: FF's at St 2 Options -b -3 / -a 3 ------------------- @@ -15,12 +15,12 @@ 3 line truncation befor 14 456789 123456789 123 -{last_modified_time} {file_name} Page 3 +{last_modified_time} {file_name} Page 3 -{last_modified_time} {file_name} Page 4 +{last_modified_time} {file_name} Page 4 15 xyzxyzxyz XYZXYZXYZ 16 456789 123456789 xyz 7 @@ -30,7 +30,7 @@ 27 no truncation before 28 no trunc -{last_modified_time} {file_name} Page 5 +{last_modified_time} {file_name} Page 5 29 xyzxyzxyz XYZXYZXYZ 30 456789 123456789 xyz 1 diff --git a/tests/fixtures/pr/a3f-0Fnt-expected b/tests/fixtures/pr/a3f-0Fnt-expected new file mode 100644 index 00000000000..14d51325b04 --- /dev/null +++ b/tests/fixtures/pr/a3f-0Fnt-expected @@ -0,0 +1,37 @@ + + +{last_modified_time} {file_name} Page 1 + + + + +{last_modified_time} {file_name} Page 2 + + +1 FF-Test: FF's at St 2 Options -b -3 / -a 3 ------------------- +4 3456789 123456789 123 5 3 Columns downwards 6 FF-Arangements: Emp +7 \ftext; \f\ntext; 8 \f\ftext; \f\f\ntex 9 3456789 123456789 123 +10 zzzzzzzzzzzzzzzzzzz 1 2 +3 line truncation befor 14 456789 123456789 123 + + +{last_modified_time} {file_name} Page 3 + + + + +{last_modified_time} {file_name} Page 4 + + +15 xyzxyzxyz XYZXYZXYZ 16 456789 123456789 xyz 7 +8 9 3456789 ab 20 DEFGHI 123 +1 2 3 +4 5 6 +27 no truncation before 28 no trunc + + +{last_modified_time} {file_name} Page 5 + + +29 xyzxyzxyz XYZXYZXYZ 30 456789 123456789 xyz 1 +2 3456789 abcdefghi 3 \ No newline at end of file diff --git a/tests/fixtures/pr/column.log.expected b/tests/fixtures/pr/column.log.expected index e548d41284f..6e817eced33 100644 --- a/tests/fixtures/pr/column.log.expected +++ b/tests/fixtures/pr/column.log.expected @@ -1,6 +1,6 @@ -{last_modified_time} column.log Page 3 +{last_modified_time} column.log Page 3 337 337 393 393 449 449 @@ -66,7 +66,7 @@ -{last_modified_time} column.log Page 4 +{last_modified_time} column.log Page 4 505 505 561 561 617 617 @@ -132,7 +132,7 @@ -{last_modified_time} column.log Page 5 +{last_modified_time} column.log Page 5 673 673 729 729 785 785 diff --git a/tests/fixtures/pr/column_across.log.expected b/tests/fixtures/pr/column_across.log.expected index 9d5a1dc1ca4..4b0c93856b8 100644 --- a/tests/fixtures/pr/column_across.log.expected +++ b/tests/fixtures/pr/column_across.log.expected @@ -1,6 +1,6 @@ -{last_modified_time} column.log Page 3 +{last_modified_time} column.log Page 3 337 337 338 338 339 339 @@ -66,7 +66,7 @@ -{last_modified_time} column.log Page 4 +{last_modified_time} column.log Page 4 505 505 506 506 507 507 @@ -132,7 +132,7 @@ -{last_modified_time} column.log Page 5 +{last_modified_time} column.log Page 5 673 673 674 674 675 675 diff --git a/tests/fixtures/pr/column_across_sep.log.expected b/tests/fixtures/pr/column_across_sep.log.expected index 65c3e71c8ff..aad7dff2750 100644 --- a/tests/fixtures/pr/column_across_sep.log.expected +++ b/tests/fixtures/pr/column_across_sep.log.expected @@ -1,6 +1,6 @@ -{last_modified_time} column.log Page 3 +{last_modified_time} column.log Page 3 337 337 | 338 338 | 339 339 @@ -66,7 +66,7 @@ -{last_modified_time} column.log Page 4 +{last_modified_time} column.log Page 4 505 505 | 506 506 | 507 507 @@ -132,7 +132,7 @@ -{last_modified_time} column.log Page 5 +{last_modified_time} column.log Page 5 673 673 | 674 674 | 675 675 diff --git a/tests/fixtures/pr/column_across_sep1.log.expected b/tests/fixtures/pr/column_across_sep1.log.expected index f9dd454d708..e28885a4e35 100644 --- a/tests/fixtures/pr/column_across_sep1.log.expected +++ b/tests/fixtures/pr/column_across_sep1.log.expected @@ -1,6 +1,6 @@ -{last_modified_time} column.log Page 3 +{last_modified_time} column.log Page 3 337 337 divide 338 338 divide 339 339 @@ -66,7 +66,7 @@ -{last_modified_time} column.log Page 4 +{last_modified_time} column.log Page 4 505 505 divide 506 506 divide 507 507 @@ -132,7 +132,7 @@ -{last_modified_time} column.log Page 5 +{last_modified_time} column.log Page 5 673 673 divide 674 674 divide 675 675 diff --git a/tests/fixtures/pr/column_spaces_across.log.expected b/tests/fixtures/pr/column_spaces_across.log.expected index 037dd814ba3..77303249bfc 100644 --- a/tests/fixtures/pr/column_spaces_across.log.expected +++ b/tests/fixtures/pr/column_spaces_across.log.expected @@ -1,6 +1,6 @@ -{last_modified_time} column.log Page 3 +{last_modified_time} column.log Page 3 337 337 338 338 339 339 @@ -66,7 +66,7 @@ -{last_modified_time} column.log Page 4 +{last_modified_time} column.log Page 4 505 505 506 506 507 507 @@ -132,7 +132,7 @@ -{last_modified_time} column.log Page 5 +{last_modified_time} column.log Page 5 673 673 674 674 675 675 diff --git a/tests/fixtures/pr/joined.log.expected b/tests/fixtures/pr/joined.log.expected index a9cee6e4f4b..4176944a6f8 100644 --- a/tests/fixtures/pr/joined.log.expected +++ b/tests/fixtures/pr/joined.log.expected @@ -1,6 +1,6 @@ -{last_modified_time} Page 1 +{last_modified_time} Page 1 ##ntation processAirPortStateChanges]: pppConnectionState 0 @@ -66,7 +66,7 @@ Mon Dec 10 11:42:59.352 Info: 802.1X changed -{last_modified_time} Page 2 +{last_modified_time} Page 2 Mon Dec 10 11:42:59.354 Info: -[AirPortExtraImplementation processAirPortStateChanges]: pppConnectionState 0 diff --git a/tests/fixtures/pr/l24-FF b/tests/fixtures/pr/l24-FF index de219b2fb1f..2da241047cb 100644 --- a/tests/fixtures/pr/l24-FF +++ b/tests/fixtures/pr/l24-FF @@ -1,6 +1,6 @@ -{last_modified_time} {file_name} Page 1 +{last_modified_time} {file_name} Page 1 1 FF-Test: FF's in Text V @@ -24,7 +24,7 @@ -{last_modified_time} {file_name} Page 2 +{last_modified_time} {file_name} Page 2 @@ -48,7 +48,7 @@ -{last_modified_time} {file_name} Page 3 +{last_modified_time} {file_name} Page 3 @@ -72,7 +72,7 @@ -{last_modified_time} {file_name} Page 4 +{last_modified_time} {file_name} Page 4 15 xyzxyzxyz XYZXYZXYZ abcabcab @@ -96,7 +96,7 @@ -{last_modified_time} {file_name} Page 5 +{last_modified_time} {file_name} Page 5 @@ -120,7 +120,7 @@ -{last_modified_time} {file_name} Page 6 +{last_modified_time} {file_name} Page 6 @@ -144,7 +144,7 @@ -{last_modified_time} {file_name} Page 7 +{last_modified_time} {file_name} Page 7 29 xyzxyzxyz XYZXYZXYZ abcabcab @@ -168,7 +168,7 @@ -{last_modified_time} {file_name} Page 8 +{last_modified_time} {file_name} Page 8 @@ -192,7 +192,7 @@ -{last_modified_time} {file_name} Page 9 +{last_modified_time} {file_name} Page 9 @@ -216,7 +216,7 @@ -{last_modified_time} {file_name} Page 10 +{last_modified_time} {file_name} Page 10 @@ -240,7 +240,7 @@ -{last_modified_time} {file_name} Page 11 +{last_modified_time} {file_name} Page 11 43 xyzxyzxyz XYZXYZXYZ abcabcab @@ -264,7 +264,7 @@ -{last_modified_time} {file_name} Page 12 +{last_modified_time} {file_name} Page 12 @@ -288,7 +288,7 @@ -{last_modified_time} {file_name} Page 13 +{last_modified_time} {file_name} Page 13 57 xyzxyzxyz XYZXYZXYZ abcabcab diff --git a/tests/fixtures/pr/mpr.log.expected b/tests/fixtures/pr/mpr.log.expected index f6fffd19141..0f4d276b108 100644 --- a/tests/fixtures/pr/mpr.log.expected +++ b/tests/fixtures/pr/mpr.log.expected @@ -1,6 +1,6 @@ -{last_modified_time} Page 1 +{last_modified_time} Page 1 1 1 ## @@ -66,7 +66,7 @@ -{last_modified_time} Page 2 +{last_modified_time} Page 2 57 57 diff --git a/tests/fixtures/pr/mpr1.log.expected b/tests/fixtures/pr/mpr1.log.expected index 64d786d90ad..1d691599878 100644 --- a/tests/fixtures/pr/mpr1.log.expected +++ b/tests/fixtures/pr/mpr1.log.expected @@ -1,6 +1,6 @@ -{last_modified_time} Page 2 +{last_modified_time} Page 2 57 57 @@ -66,7 +66,7 @@ -{last_modified_time} Page 3 +{last_modified_time} Page 3 113 113 @@ -132,7 +132,7 @@ -{last_modified_time} Page 4 +{last_modified_time} Page 4 169 169 diff --git a/tests/fixtures/pr/mpr2.log.expected b/tests/fixtures/pr/mpr2.log.expected index 091f0f2280f..9c453924ccc 100644 --- a/tests/fixtures/pr/mpr2.log.expected +++ b/tests/fixtures/pr/mpr2.log.expected @@ -1,6 +1,6 @@ -{last_modified_time} Page 1 +{last_modified_time} Page 1 1 1 ## 1 @@ -100,7 +100,7 @@ -{last_modified_time} Page 2 +{last_modified_time} Page 2 91 91 91 diff --git a/tests/fixtures/pr/stdin.log.expected b/tests/fixtures/pr/stdin.log.expected index 6922ee59454..5f9d6c23535 100644 --- a/tests/fixtures/pr/stdin.log.expected +++ b/tests/fixtures/pr/stdin.log.expected @@ -1,6 +1,6 @@ -{last_modified_time} Page 1 +{last_modified_time} Page 1 1 ntation processAirPortStateChanges]: pppConnectionState 0 @@ -66,7 +66,7 @@ -{last_modified_time} Page 2 +{last_modified_time} Page 2 57 Mon Dec 10 11:42:59.354 Info: -[AirPortExtraImplementation processAirPortStateChanges]: pppConnectionState 0 diff --git a/tests/fixtures/pr/test_num_page_2.log.expected b/tests/fixtures/pr/test_num_page_2.log.expected index dae437ef867..bf9a6c174fd 100644 --- a/tests/fixtures/pr/test_num_page_2.log.expected +++ b/tests/fixtures/pr/test_num_page_2.log.expected @@ -1,6 +1,6 @@ -{last_modified_time} test_num_page.log Page 1 +{last_modified_time} test_num_page.log Page 1 1 ntation processAirPortStateChanges]: pppConnectionState 0 @@ -66,7 +66,7 @@ -{last_modified_time} test_num_page.log Page 2 +{last_modified_time} test_num_page.log Page 2 57 ntation processAirPortStateChanges]: pppConnectionState 0 diff --git a/tests/fixtures/pr/test_num_page_char.log.expected b/tests/fixtures/pr/test_num_page_char.log.expected index 169dbd844d2..0536b75c0d8 100644 --- a/tests/fixtures/pr/test_num_page_char.log.expected +++ b/tests/fixtures/pr/test_num_page_char.log.expected @@ -1,6 +1,6 @@ -{last_modified_time} test_num_page.log Page 1 +{last_modified_time} test_num_page.log Page 1 1cntation processAirPortStateChanges]: pppConnectionState 0 @@ -66,7 +66,7 @@ -{last_modified_time} test_num_page.log Page 2 +{last_modified_time} test_num_page.log Page 2 57cntation processAirPortStateChanges]: pppConnectionState 0 diff --git a/tests/fixtures/pr/test_num_page_char_one.log.expected b/tests/fixtures/pr/test_num_page_char_one.log.expected index dd78131921e..cd0b1278115 100644 --- a/tests/fixtures/pr/test_num_page_char_one.log.expected +++ b/tests/fixtures/pr/test_num_page_char_one.log.expected @@ -1,6 +1,6 @@ -{last_modified_time} test_num_page.log Page 1 +{last_modified_time} test_num_page.log Page 1 1cntation processAirPortStateChanges]: pppConnectionState 0 @@ -66,7 +66,7 @@ -{last_modified_time} test_num_page.log Page 2 +{last_modified_time} test_num_page.log Page 2 7cntation processAirPortStateChanges]: pppConnectionState 0 diff --git a/tests/fixtures/pr/test_one_page.log.expected b/tests/fixtures/pr/test_one_page.log.expected index 54f7723924f..fc354b41d84 100644 --- a/tests/fixtures/pr/test_one_page.log.expected +++ b/tests/fixtures/pr/test_one_page.log.expected @@ -1,6 +1,6 @@ -{last_modified_time} test_one_page.log Page 1 +{last_modified_time} test_one_page.log Page 1 ntation processAirPortStateChanges]: pppConnectionState 0 diff --git a/tests/fixtures/pr/test_one_page_double_line.log.expected b/tests/fixtures/pr/test_one_page_double_line.log.expected index e32101fcf5d..49ed90c8752 100644 --- a/tests/fixtures/pr/test_one_page_double_line.log.expected +++ b/tests/fixtures/pr/test_one_page_double_line.log.expected @@ -1,6 +1,6 @@ -{last_modified_time} test_one_page.log Page 1 +{last_modified_time} test_one_page.log Page 1 ntation processAirPortStateChanges]: pppConnectionState 0 @@ -66,7 +66,7 @@ Mon Dec 10 11:42:57.751 Info: -[AirPortExtraImplementati -{last_modified_time} test_one_page.log Page 2 +{last_modified_time} test_one_page.log Page 2 Mon Dec 10 11:42:57.896 Info: 802.1X changed diff --git a/tests/fixtures/pr/test_one_page_first_line.log.expected b/tests/fixtures/pr/test_one_page_first_line.log.expected index 303f01c732b..5c7b2eebe66 100644 --- a/tests/fixtures/pr/test_one_page_first_line.log.expected +++ b/tests/fixtures/pr/test_one_page_first_line.log.expected @@ -1,6 +1,6 @@ -{last_modified_time} test_one_page.log Page 1 +{last_modified_time} test_one_page.log Page 1 5 ntation processAirPortStateChanges]: pppConnectionState 0 diff --git a/tests/fixtures/pr/test_one_page_header.log.expected b/tests/fixtures/pr/test_one_page_header.log.expected index a00d5f85505..06a69088c25 100644 --- a/tests/fixtures/pr/test_one_page_header.log.expected +++ b/tests/fixtures/pr/test_one_page_header.log.expected @@ -1,6 +1,6 @@ -{last_modified_time} {header} Page 1 +{last_modified_time} {header} Page 1 ntation processAirPortStateChanges]: pppConnectionState 0 diff --git a/tests/fixtures/pr/test_page_length.log.expected b/tests/fixtures/pr/test_page_length.log.expected index 8f4ab82d1cd..38578c1dcaa 100644 --- a/tests/fixtures/pr/test_page_length.log.expected +++ b/tests/fixtures/pr/test_page_length.log.expected @@ -1,6 +1,6 @@ -{last_modified_time} test.log Page 2 +{last_modified_time} test.log Page 2 91 Mon Dec 10 11:43:31.748 )} took 0.0025 seconds, returned 10 results @@ -100,7 +100,7 @@ -{last_modified_time} test.log Page 3 +{last_modified_time} test.log Page 3 181 Mon Dec 10 11:52:32.715 AutoJoin: Successful cache-assisted scan request for locationd with channels {( diff --git a/tests/fixtures/pr/test_page_range_1.log.expected b/tests/fixtures/pr/test_page_range_1.log.expected index f254261d4bc..fa35f844509 100644 --- a/tests/fixtures/pr/test_page_range_1.log.expected +++ b/tests/fixtures/pr/test_page_range_1.log.expected @@ -1,6 +1,6 @@ -{last_modified_time} test.log Page 15 +{last_modified_time} test.log Page 15 Mon Dec 10 12:05:48.183 [channelNumber=12(2GHz), channelWidth={20MHz}, active] @@ -66,7 +66,7 @@ Mon Dec 10 12:06:28.765 Roam: ROAMING PROFILES updated to SINGLE -{last_modified_time} test.log Page 16 +{last_modified_time} test.log Page 16 Mon Dec 10 12:06:28.770 SC: airportdProcessSystemConfigurationEvent: Processing 'State:/Network/Interface/en0/AirPort/ProfileID' @@ -132,7 +132,7 @@ Mon Dec 10 12:06:50.945 BTC: __BluetoothCoexHandleUpdateForNode: -{last_modified_time} test.log Page 17 +{last_modified_time} test.log Page 17 Mon Dec 10 12:06:50.945 BTC: BluetoothCoexSetProfile: profile for band 2.4GHz didn't change @@ -198,7 +198,7 @@ Mon Dec 10 12:13:27.640 Info: link quality changed -{last_modified_time} test.log Page 18 +{last_modified_time} test.log Page 18 Mon Dec 10 12:14:46.658 Info: SCAN request received from pid 92 (locationd) with priority 2 diff --git a/tests/fixtures/pr/test_page_range_2.log.expected b/tests/fixtures/pr/test_page_range_2.log.expected index 4f260eb6544..2ca5ed04dca 100644 --- a/tests/fixtures/pr/test_page_range_2.log.expected +++ b/tests/fixtures/pr/test_page_range_2.log.expected @@ -1,6 +1,6 @@ -{last_modified_time} test.log Page 15 +{last_modified_time} test.log Page 15 Mon Dec 10 12:05:48.183 [channelNumber=12(2GHz), channelWidth={20MHz}, active] @@ -66,7 +66,7 @@ Mon Dec 10 12:06:28.765 Roam: ROAMING PROFILES updated to SINGLE -{last_modified_time} test.log Page 16 +{last_modified_time} test.log Page 16 Mon Dec 10 12:06:28.770 SC: airportdProcessSystemConfigurationEvent: Processing 'State:/Network/Interface/en0/AirPort/ProfileID' @@ -132,7 +132,7 @@ Mon Dec 10 12:06:50.945 BTC: __BluetoothCoexHandleUpdateForNode: -{last_modified_time} test.log Page 17 +{last_modified_time} test.log Page 17 Mon Dec 10 12:06:50.945 BTC: BluetoothCoexSetProfile: profile for band 2.4GHz didn't change diff --git a/tests/uudoc/mod.rs b/tests/uudoc/mod.rs index 4be9803b80c..455d3984f0e 100644 --- a/tests/uudoc/mod.rs +++ b/tests/uudoc/mod.rs @@ -132,3 +132,39 @@ fn test_manpage_base64() { assert!(output_str.contains("base64 alphabet")); assert!(!output_str.to_ascii_lowercase().contains("base32")); } + +// Test to ensure markdown headers are correctly formatted in generated markdown files +// Prevents regression of https://github.com/uutils/coreutils/issues/10003 +#[test] +fn test_markdown_header_format() { + use std::fs; + + // Read a sample markdown file from the documentation + // This assumes the docs have been generated (they should be in the repo) + let docs_path = "docs/src/utils/cat.md"; + + if fs::metadata(docs_path).is_ok() { + let content = + fs::read_to_string(docs_path).expect("Failed to read generated markdown file"); + + // Verify Options header is in markdown format (## Options) + assert!( + content.contains("## Options"), + "Generated markdown should contain '## Options' header" + ); + + // Verify no HTML h2 tags for Options (old format) + assert!( + !content.contains("

Options

"), + "Generated markdown should not contain '

Options

' (use markdown format instead)" + ); + + // Also verify Examples if it exists + if content.contains("## Examples") { + assert!( + content.contains("## Examples"), + "Generated markdown should contain '## Examples' header in markdown format" + ); + } + } +} diff --git a/util/build-gnu.sh b/util/build-gnu.sh index 15c72720fc8..6075c863727 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -22,8 +22,8 @@ REPO_main_dir="$(dirname -- "${ME_dir}")" : ${PROFILE:=debug} # default profile -export PROFILE -CARGO_FEATURE_FLAGS="" +export PROFILE # tell to make +unset CARGOFLAGS ### * config (from environment with fallback defaults); note: GNU is expected to be a sibling repo directory @@ -63,15 +63,15 @@ echo "UU_BUILD_DIR='${UU_BUILD_DIR}'" cd "${path_UUTILS}" && echo "[ pwd:'${PWD}' ]" export SELINUX_ENABLED # Run this script with=1 for testing SELinux -[ "${SELINUX_ENABLED}" = 1 ] && CARGO_FEATURE_FLAGS="${CARGO_FEATURE_FLAGS} selinux" +[ "${SELINUX_ENABLED}" = 1 ] && CARGOFLAGS="${CARGOFLAGS} selinux" # Trim leading whitespace from feature flags -CARGO_FEATURE_FLAGS="$(echo "${CARGO_FEATURE_FLAGS}" | sed -e 's/^[[:space:]]*//')" +CARGOFLAGS="$(echo "${CARGOFLAGS}" | sed -e 's/^[[:space:]]*//')" # If we have feature flags, format them correctly for cargo -if [ ! -z "${CARGO_FEATURE_FLAGS}" ]; then - CARGO_FEATURE_FLAGS="--features ${CARGO_FEATURE_FLAGS}" - echo "Building with cargo flags: ${CARGO_FEATURE_FLAGS}" +if [ ! -z "${CARGOFLAGS}" ]; then + CARGOFLAGS="--features ${CARGOFLAGS}" + echo "Building with cargo flags: ${CARGOFLAGS}" fi # Set up quilt for patch management @@ -87,15 +87,16 @@ else fi cd - +export CARGOFLAGS # tell to make # bug: seq with MULTICALL=y breaks env-signal-handler.sh - "${MAKE}" UTILS="install seq" PROFILE="${PROFILE}" CARGOFLAGS="${CARGO_FEATURE_FLAGS}" + "${MAKE}" UTILS="install seq" ln -vf "${UU_BUILD_DIR}/install" "${UU_BUILD_DIR}/ginstall" # The GNU tests use renamed install to ginstall if [ "${SELINUX_ENABLED}" = 1 ];then # Build few utils for SELinux for faster build. MULTICALL=y fails... - "${MAKE}" UTILS="cat chcon cp cut echo env groups id ln ls mkdir mkfifo mknod mktemp mv printf rm rmdir runcon stat test touch tr true uname wc whoami" PROFILE="${PROFILE}" CARGOFLAGS="${CARGO_FEATURE_FLAGS}" + "${MAKE}" UTILS="cat chcon chmod cp cut echo env groups id ln ls mkdir mkfifo mknod mktemp mv printf rm rmdir runcon stat test touch tr true uname wc whoami" else # Use MULTICALL=y for faster build - "${MAKE}" MULTICALL=y SKIP_UTILS="install more seq" PROFILE="${PROFILE}" CARGOFLAGS="${CARGO_FEATURE_FLAGS}" + "${MAKE}" MULTICALL=y SKIP_UTILS="install more seq" for binary in $("${UU_BUILD_DIR}"/coreutils --list) do ln -vf "${UU_BUILD_DIR}/coreutils" "${UU_BUILD_DIR}/${binary}" done @@ -109,9 +110,7 @@ cd "${path_GNU}" && echo "[ pwd:'${PWD}' ]" # Note that some test (e.g. runcon/runcon-compute.sh) incorrectly passes by this for binary in $(./build-aux/gen-lists-of-programs.sh --list-progs); do bin_path="${UU_BUILD_DIR}/${binary}" - test -f "${bin_path}" || { - cp -v /usr/bin/false "${bin_path}" - } + test -f "${bin_path}" || cp -v /usr/bin/false "${bin_path}" done # Always update the PATH to test the uutils coreutils instead of the GNU coreutils @@ -125,6 +124,8 @@ if test -f gnu-built; then else # Disable useless checks "${SED}" -i 's|check-texinfo: $(syntax_checks)|check-texinfo:|' doc/local.mk + # Stop manpage generation for cleaner log + : > man/local.mk # Use CFLAGS for best build time since we discard GNU coreutils CFLAGS="${CFLAGS} -pipe -O0 -s" ./configure -C --quiet --disable-gcc-warnings --disable-nls --disable-dependency-tracking --disable-bold-man-page-references \ --enable-single-binary=symlinks --enable-install-program="arch,kill,uptime,hostname" \ @@ -137,6 +138,7 @@ else # Skip make if possible # Use GNU nproc for *BSD and macOS NPROC="$(command -v nproc||command -v gnproc)" + test "${SELINUX_ENABLED}" = 1 && touch src/getlimits # SELinux tests does not use it test -f src/getlimits || "${MAKE}" -j "$("${NPROC}")" cp -f src/getlimits "${UU_BUILD_DIR}" @@ -318,9 +320,9 @@ echo "n_stat1 = \$n_stat1"\n\ echo "n_stat2 = \$n_stat2"\n\ test \$n_stat1 -ge \$n_stat2 \\' tests/ls/stat-free-color.sh -# no need to replicate this output with hashsum +# clap changes the error message. Check exit code only. "${SED}" -i -e "s|Try 'md5sum --help' for more information.\\\n||" tests/cksum/md5sum.pl - +"${SED}" -i '/check-ignore-missing-4/,/EXIT/c \ ['\''check-ignore-missing-4'\'', '\''--ignore-missing'\'', {IN=> {f=> '\'''\''}}, {ERR_SUBST=>"s/.*//s"}, {EXIT=> 1}],' tests/cksum/md5sum.pl # Our ls command always outputs ANSI color codes prepended with a zero. However, # in the case of GNU, it seems inconsistent. Nevertheless, it looks like it # doesn't matter whether we prepend a zero or not. diff --git a/util/fetch-gnu.sh b/util/fetch-gnu.sh index 92e88ed75c6..caecdec7da3 100755 --- a/util/fetch-gnu.sh +++ b/util/fetch-gnu.sh @@ -7,6 +7,8 @@ curl -L "${repo}/releases/download/v${ver}/coreutils-${ver}.tar.xz" | tar --stri curl -L ${repo}/raw/refs/heads/master/tests/mv/hardlink-case.sh > tests/mv/hardlink-case.sh curl -L ${repo}/raw/refs/heads/master/tests/mkdir/writable-under-readonly.sh > tests/mkdir/writable-under-readonly.sh curl -L ${repo}/raw/refs/heads/master/tests/cp/cp-mv-enotsup-xattr.sh > tests/cp/cp-mv-enotsup-xattr.sh #spell-checker:disable-line +curl -L ${repo}/raw/refs/heads/master/tests/cp/nfs-removal-race.sh > tests/cp/nfs-removal-race.sh curl -L ${repo}/raw/refs/heads/master/tests/csplit/csplit-io-err.sh > tests/csplit/csplit-io-err.sh +curl -L ${repo}/raw/refs/heads/master/tests/stty/bad-speed.sh > tests/stty/bad-speed.sh # Avoid incorrect PASS curl -L ${repo}/raw/refs/heads/master/tests/runcon/runcon-compute.sh > tests/runcon/runcon-compute.sh