diff --git a/.clippy.toml b/.clippy.toml index 89fd1cccd..6339ccf21 100644 --- a/.clippy.toml +++ b/.clippy.toml @@ -1,4 +1,4 @@ -msrv = "1.70.0" +msrv = "1.79.0" cognitive-complexity-threshold = 24 missing-docs-in-crate-items = true check-private-items = true diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index c8993b121..e4c0fa345 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -11,7 +11,7 @@ env: PROJECT_NAME: coreutils PROJECT_DESC: "Core universal (cross-platform) utilities" PROJECT_AUTH: "uutils" - RUST_MIN_SRV: "1.70.0" + RUST_MIN_SRV: "1.79.0" # * style job configuration STYLE_FAIL_ON_FAULT: true ## (bool) fail the build if a style job contains a fault (error or warning); may be overridden on a per-job basis @@ -37,6 +37,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: EmbarkStudios/cargo-deny-action@v2 style_deps: @@ -54,6 +56,8 @@ jobs: - { os: windows-latest , features: feat_os_windows } steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: dtolnay/rust-toolchain@nightly ## note: requires 'nightly' toolchain b/c `cargo-udeps` uses the `rustc` '-Z save-analysis' option ## * ... ref: @@ -106,13 +110,15 @@ jobs: # - { os: windows-latest , features: feat_os_windows } steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: dtolnay/rust-toolchain@master with: toolchain: stable components: clippy - uses: Swatinem/rust-cache@v2 - name: Run sccache-cache - uses: mozilla-actions/sccache-action@v0.0.6 + uses: mozilla-actions/sccache-action@v0.0.7 - name: Initialize workflow variables id: vars shell: bash @@ -139,7 +145,7 @@ jobs: shell: bash run: | RUSTDOCFLAGS="-Dwarnings" cargo doc ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} --no-deps --workspace --document-private-items - - uses: DavidAnson/markdownlint-cli2-action@v17 + - uses: DavidAnson/markdownlint-cli2-action@v18 with: fix: "true" globs: | @@ -159,6 +165,8 @@ jobs: - { os: ubuntu-latest , features: feat_os_unix } steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: dtolnay/rust-toolchain@master with: toolchain: ${{ env.RUST_MIN_SRV }} @@ -166,7 +174,7 @@ jobs: - uses: taiki-e/install-action@nextest - uses: Swatinem/rust-cache@v2 - name: Run sccache-cache - uses: mozilla-actions/sccache-action@v0.0.6 + uses: mozilla-actions/sccache-action@v0.0.7 - name: Initialize workflow variables id: vars shell: bash @@ -227,6 +235,8 @@ jobs: - { os: ubuntu-latest , features: feat_os_unix } steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - name: "`cargo update` testing" @@ -250,11 +260,13 @@ jobs: - { os: ubuntu-latest , features: feat_os_unix } steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: dtolnay/rust-toolchain@stable - uses: taiki-e/install-action@nextest - uses: Swatinem/rust-cache@v2 - name: Run sccache-cache - uses: mozilla-actions/sccache-action@v0.0.6 + uses: mozilla-actions/sccache-action@v0.0.7 - name: "`make build`" shell: bash run: | @@ -304,11 +316,13 @@ jobs: - { os: windows-latest , features: feat_os_windows } steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: dtolnay/rust-toolchain@stable - uses: taiki-e/install-action@nextest - uses: Swatinem/rust-cache@v2 - name: Run sccache-cache - uses: mozilla-actions/sccache-action@v0.0.6 + uses: mozilla-actions/sccache-action@v0.0.7 - name: Test run: cargo nextest run --hide-progress-bar --profile ci --features ${{ matrix.job.features }} env: @@ -331,11 +345,13 @@ jobs: - { os: windows-latest , features: feat_os_windows } steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: dtolnay/rust-toolchain@nightly - uses: taiki-e/install-action@nextest - uses: Swatinem/rust-cache@v2 - name: Run sccache-cache - uses: mozilla-actions/sccache-action@v0.0.6 + uses: mozilla-actions/sccache-action@v0.0.7 - name: Test run: cargo nextest run --hide-progress-bar --profile ci --features ${{ matrix.job.features }} env: @@ -355,10 +371,12 @@ jobs: - { os: ubuntu-latest , features: feat_os_unix } steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - name: Run sccache-cache - uses: mozilla-actions/sccache-action@v0.0.6 + uses: mozilla-actions/sccache-action@v0.0.7 - name: Install dependencies shell: bash run: | @@ -397,14 +415,14 @@ jobs: --arg multisize "$SIZE_MULTI" \ '{($date): { sha: $sha, size: $size, multisize: $multisize, }}' > size-result.json - name: Download the previous individual size result - uses: dawidd6/action-download-artifact@v6 + uses: dawidd6/action-download-artifact@v7 with: workflow: CICD.yml name: individual-size-result repo: uutils/coreutils path: dl - name: Download the previous size result - uses: dawidd6/action-download-artifact@v6 + uses: dawidd6/action-download-artifact@v7 with: workflow: CICD.yml name: size-result @@ -485,6 +503,8 @@ jobs: - { os: windows-latest , target: aarch64-pc-windows-msvc , features: feat_os_windows, use-cross: use-cross , skip-tests: true } steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: dtolnay/rust-toolchain@master with: toolchain: ${{ env.RUST_MIN_SRV }} @@ -493,7 +513,7 @@ jobs: with: key: "${{ matrix.job.os }}_${{ matrix.job.target }}" - name: Run sccache-cache - uses: mozilla-actions/sccache-action@v0.0.6 + uses: mozilla-actions/sccache-action@v0.0.7 - name: Initialize workflow variables id: vars shell: bash @@ -753,6 +773,7 @@ jobs: uses: softprops/action-gh-release@v2 if: steps.vars.outputs.DEPLOY with: + draft: true files: | ${{ steps.vars.outputs.STAGING }}/${{ steps.vars.outputs.PKG_NAME }} ${{ steps.vars.outputs.STAGING }}/${{ steps.vars.outputs.DPKG_NAME }} @@ -779,9 +800,11 @@ jobs: ## VARs setup echo "TEST_SUMMARY_FILE=busybox-result.json" >> $GITHUB_OUTPUT - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: Swatinem/rust-cache@v2 - name: Run sccache-cache - uses: mozilla-actions/sccache-action@v0.0.6 + uses: mozilla-actions/sccache-action@v0.0.7 - name: Install/setup prerequisites shell: bash run: | @@ -859,13 +882,15 @@ jobs: TEST_SUMMARY_FILE="toybox-result.json" outputs TEST_SUMMARY_FILE - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: dtolnay/rust-toolchain@master with: toolchain: ${{ env.RUST_MIN_SRV }} components: rustfmt - uses: Swatinem/rust-cache@v2 - name: Run sccache-cache - uses: mozilla-actions/sccache-action@v0.0.6 + uses: mozilla-actions/sccache-action@v0.0.7 - name: Build coreutils as multiple binaries shell: bash run: | @@ -934,6 +959,8 @@ jobs: os: [ubuntu-latest, macos-latest, windows-latest] steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - name: build and test all programs individually diff --git a/.github/workflows/CheckScripts.yml b/.github/workflows/CheckScripts.yml index c18c4733c..4800cd285 100644 --- a/.github/workflows/CheckScripts.yml +++ b/.github/workflows/CheckScripts.yml @@ -30,6 +30,8 @@ jobs: contents: read steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - name: Run ShellCheck uses: ludeeus/action-shellcheck@master env: @@ -46,6 +48,8 @@ jobs: contents: read steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - name: Setup shfmt uses: mfinelli/setup-shfmt@v3 - name: Run shfmt diff --git a/.github/workflows/FixPR.yml b/.github/workflows/FixPR.yml index e837b3546..5cd7fe647 100644 --- a/.github/workflows/FixPR.yml +++ b/.github/workflows/FixPR.yml @@ -27,6 +27,8 @@ jobs: - { os: ubuntu-latest , features: feat_os_unix } steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - name: Initialize job variables id: vars shell: bash @@ -86,6 +88,8 @@ jobs: - { os: ubuntu-latest , features: feat_os_unix } steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - name: Initialize job variables id: vars shell: bash diff --git a/.github/workflows/GnuComment.yml b/.github/workflows/GnuComment.yml index 36c54490c..987343723 100644 --- a/.github/workflows/GnuComment.yml +++ b/.github/workflows/GnuComment.yml @@ -4,7 +4,7 @@ on: workflow_run: workflows: ["GnuTests"] types: - - completed + - completed # zizmor: ignore[dangerous-triggers] permissions: {} jobs: diff --git a/.github/workflows/GnuTests.yml b/.github/workflows/GnuTests.yml index 113cb1e97..0b9d8ce7f 100644 --- a/.github/workflows/GnuTests.yml +++ b/.github/workflows/GnuTests.yml @@ -23,6 +23,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} +env: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + jobs: gnu: permissions: @@ -45,9 +48,9 @@ jobs: path_reference="reference" outputs path_GNU path_GNU_tests path_reference path_UUTILS # - repo_default_branch="${{ github.event.repository.default_branch }}" + repo_default_branch="$DEFAULT_BRANCH" repo_GNU_ref="v9.5" - repo_reference_branch="${{ github.event.repository.default_branch }}" + repo_reference_branch="$DEFAULT_BRANCH" outputs repo_default_branch repo_GNU_ref repo_reference_branch # SUITE_LOG_FILE="${path_GNU_tests}/test-suite.log" @@ -62,6 +65,7 @@ jobs: uses: actions/checkout@v4 with: path: '${{ steps.vars.outputs.path_UUTILS }}' + persist-credentials: false - uses: dtolnay/rust-toolchain@master with: toolchain: stable @@ -76,6 +80,7 @@ jobs: path: '${{ steps.vars.outputs.path_GNU }}' ref: ${{ steps.vars.outputs.repo_GNU_ref }} submodules: false + persist-credentials: false - name: Override submodule URL and initialize submodules # Use github instead of upstream git server @@ -86,7 +91,7 @@ jobs: working-directory: ${{ steps.vars.outputs.path_GNU }} - name: Retrieve reference artifacts - uses: dawidd6/action-download-artifact@v6 + uses: dawidd6/action-download-artifact@v7 # ref: continue-on-error: true ## don't break the build for missing reference artifacts (may be expired or just not generated yet) with: @@ -244,11 +249,16 @@ jobs: CURRENT_RUN_ERROR=$(sed -n "s/^ERROR: \([[:print:]]\+\).*/\1/p" "${new_log_file}" | sort) REF_FAILING=$(sed -n "s/^FAIL: \([[:print:]]\+\).*/\1/p" "${ref_log_file}"| sort) CURRENT_RUN_FAILING=$(sed -n "s/^FAIL: \([[:print:]]\+\).*/\1/p" "${new_log_file}" | sort) - echo "Detailled information:" + REF_SKIP=$(sed -n "s/^SKIP: \([[:print:]]\+\).*/\1/p" "${ref_log_file}"| sort) + CURRENT_RUN_SKIP=$(sed -n "s/^SKIP: \([[:print:]]\+\).*/\1/p" "${new_log_file}" | sort) + + echo "Detailed information:" echo "REF_ERROR = ${REF_ERROR}" echo "CURRENT_RUN_ERROR = ${CURRENT_RUN_ERROR}" echo "REF_FAILING = ${REF_FAILING}" echo "CURRENT_RUN_FAILING = ${CURRENT_RUN_FAILING}" + echo "REF_SKIP_PASS = ${REF_SKIP}" + echo "CURRENT_RUN_SKIP = ${CURRENT_RUN_SKIP}" # Compare failing and error tests for LINE in ${CURRENT_RUN_FAILING} @@ -303,11 +313,22 @@ jobs: do if ! grep -Fxq ${LINE}<<<"${CURRENT_RUN_ERROR}" then - MSG="Congrats! The gnu test ${LINE} is no longer ERROR!" + MSG="Congrats! The gnu test ${LINE} is no longer ERROR! (might be PASS or FAIL)" echo "::warning ::$MSG" echo $MSG >> ${COMMENT_LOG} fi done + + for LINE in ${REF_SKIP} + do + if ! grep -Fxq ${LINE}<<<"${CURRENT_RUN_SKIP}" + then + MSG="Congrats! The gnu test ${LINE} is no longer SKIP! (might be PASS, ERROR or FAIL)" + echo "::warning ::$MSG" + echo $MSG >> ${COMMENT_LOG} + fi + done + else echo "::warning ::Skipping ${test_type} test failure comparison; no prior reference test logs are available." fi diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index d920ad801..a7dcbdbbd 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -79,6 +79,8 @@ jobs: sudo udevadm control --reload-rules sudo udevadm trigger --name-match=kvm - uses: actions/checkout@v4 + with: + persist-credentials: false - name: Collect information about runner if: always() continue-on-error: true @@ -176,7 +178,7 @@ jobs: util/android-commands.sh sync_host util/android-commands.sh build util/android-commands.sh tests - if [[ "${{ steps.rust-cache.outputs.cache-hit }}" != 'true' ]]; then util/android-commands.sh sync_image; fi; exit 0 + if [ "${{ steps.rust-cache.outputs.cache-hit }}" != 'true' ]; then util/android-commands.sh sync_image; fi; exit 0 - name: Collect information about runner ressources if: always() continue-on-error: true diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index cd1334c2e..814da316a 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -32,6 +32,8 @@ jobs: - { os: ubuntu-latest , features: feat_os_unix } steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: dtolnay/rust-toolchain@master with: toolchain: stable @@ -44,7 +46,7 @@ jobs: ## VARs setup outputs() { step_id="${{ github.action }}"; for var in "$@" ; do echo steps.${step_id}.outputs.${var}="${!var}"; echo "${var}=${!var}" >> $GITHUB_OUTPUT; done; } # failure mode - unset FAIL_ON_FAULT ; case '${{ env.STYLE_FAIL_ON_FAULT }}' in + unset FAIL_ON_FAULT ; case "$STYLE_FAIL_ON_FAULT" in ''|0|f|false|n|no|off) FAULT_TYPE=warning ;; *) FAIL_ON_FAULT=true ; FAULT_TYPE=error ;; esac; @@ -75,13 +77,15 @@ jobs: - { os: windows-latest , features: feat_os_windows } steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: dtolnay/rust-toolchain@master with: toolchain: stable components: clippy - uses: Swatinem/rust-cache@v2 - name: Run sccache-cache - uses: mozilla-actions/sccache-action@v0.0.6 + uses: mozilla-actions/sccache-action@v0.0.7 - name: Initialize workflow variables id: vars shell: bash @@ -89,7 +93,7 @@ jobs: ## VARs setup outputs() { step_id="${{ github.action }}"; for var in "$@" ; do echo steps.${step_id}.outputs.${var}="${!var}"; echo "${var}=${!var}" >> $GITHUB_OUTPUT; done; } # failure mode - unset FAIL_ON_FAULT ; case '${{ env.STYLE_FAIL_ON_FAULT }}' in + unset FAIL_ON_FAULT ; case "$STYLE_FAIL_ON_FAULT" in ''|0|f|false|n|no|off) FAULT_TYPE=warning ;; *) FAIL_ON_FAULT=true ; FAULT_TYPE=error ;; esac; @@ -120,6 +124,8 @@ jobs: - { os: ubuntu-latest , features: feat_os_unix } steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - name: Initialize workflow variables id: vars shell: bash @@ -127,7 +133,7 @@ jobs: ## VARs setup outputs() { step_id="${{ github.action }}"; for var in "$@" ; do echo steps.${step_id}.outputs.${var}="${!var}"; echo "${var}=${!var}" >> $GITHUB_OUTPUT; done; } # failure mode - unset FAIL_ON_FAULT ; case '${{ env.STYLE_FAIL_ON_FAULT }}' in + unset FAIL_ON_FAULT ; case "$STYLE_FAIL_ON_FAULT" in ''|0|f|false|n|no|off) FAULT_TYPE=warning ;; *) FAIL_ON_FAULT=true ; FAULT_TYPE=error ;; esac; @@ -156,6 +162,8 @@ jobs: steps: - name: Clone repository uses: actions/checkout@v4 + with: + persist-credentials: false - name: Check run: npx --yes @taplo/cli fmt --check diff --git a/.github/workflows/freebsd.yml b/.github/workflows/freebsd.yml index b31ac3353..27ff2afe4 100644 --- a/.github/workflows/freebsd.yml +++ b/.github/workflows/freebsd.yml @@ -35,11 +35,13 @@ jobs: RUSTC_WRAPPER: "sccache" steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: Swatinem/rust-cache@v2 - name: Run sccache-cache - uses: mozilla-actions/sccache-action@v0.0.6 + uses: mozilla-actions/sccache-action@v0.0.7 - name: Prepare, build and test - uses: vmactions/freebsd-vm@v1.1.5 + uses: vmactions/freebsd-vm@v1.1.6 with: usesh: true sync: rsync @@ -127,11 +129,13 @@ jobs: RUSTC_WRAPPER: "sccache" steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: Swatinem/rust-cache@v2 - name: Run sccache-cache - uses: mozilla-actions/sccache-action@v0.0.6 + uses: mozilla-actions/sccache-action@v0.0.7 - name: Prepare, build and test - uses: vmactions/freebsd-vm@v1.1.5 + uses: vmactions/freebsd-vm@v1.1.6 with: usesh: true sync: rsync diff --git a/.github/workflows/fuzzing.yml b/.github/workflows/fuzzing.yml index 3adc52734..c8e2c8014 100644 --- a/.github/workflows/fuzzing.yml +++ b/.github/workflows/fuzzing.yml @@ -22,6 +22,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: dtolnay/rust-toolchain@nightly - name: Install `cargo-fuzz` run: cargo install cargo-fuzz @@ -63,6 +65,8 @@ jobs: steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: dtolnay/rust-toolchain@nightly - name: Install `cargo-fuzz` run: cargo install cargo-fuzz diff --git a/.vscode/cspell.dictionaries/jargon.wordlist.txt b/.vscode/cspell.dictionaries/jargon.wordlist.txt index c2e01f508..4109630e5 100644 --- a/.vscode/cspell.dictionaries/jargon.wordlist.txt +++ b/.vscode/cspell.dictionaries/jargon.wordlist.txt @@ -10,6 +10,7 @@ bytewise canonicalization canonicalize canonicalizing +capget codepoint codepoints codegen @@ -65,6 +66,7 @@ kibi kibibytes libacl lcase +llistxattr lossily lstat mebi @@ -108,6 +110,7 @@ seedable semver semiprime semiprimes +setcap setfacl shortcode shortcodes @@ -157,6 +160,8 @@ retval subdir val vals +inval +nofield # * clippy uninlined diff --git a/Cargo.lock b/Cargo.lock index b59405071..642b3fdda 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,10 +3,10 @@ version = 3 [[package]] -name = "adler" -version = "1.0.2" +name = "adler2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] name = "ahash" @@ -61,23 +61,24 @@ dependencies = [ [[package]] name = "anstream" -version = "0.5.0" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", + "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.0" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "anstyle-parse" @@ -99,12 +100,12 @@ dependencies = [ [[package]] name = "anstyle-wincon" -version = "2.1.0" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" dependencies = [ "anstyle", - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] @@ -136,9 +137,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "bigdecimal" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f850665a0385e070b64c38d2354e6c104c8479c59868d1e48a0c13ee2c7a1c1" +checksum = "7f31f3af01c5c65a07985c804d3366560e6fa7883d640a122819b14ec327482c" dependencies = [ "autocfg", "libm", @@ -182,24 +183,9 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.86", + "syn 2.0.87", ] -[[package]] -name = "bit-set" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" -dependencies = [ - "bit-vec", -] - -[[package]] -name = "bit-vec" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" - [[package]] name = "bitflags" version = "1.3.2" @@ -237,9 +223,9 @@ dependencies = [ [[package]] name = "blake3" -version = "1.5.4" +version = "1.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d82033247fd8e890df8f740e407ad4d038debb9eb1f40533fffb32e7d17dc6f7" +checksum = "b8ee0c1824c4dea5b5f81736aff91bae041d2c07ee1192bec91054e10e3e601e" dependencies = [ "arrayref", "arrayvec", @@ -259,9 +245,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.10.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c" +checksum = "786a307d683a5bf92e6fd5fd69a7eb613751668d1d8d67d802846dfe367c62c8" dependencies = [ "memchr", "regex-automata", @@ -270,9 +256,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.11.1" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytecount" @@ -318,9 +304,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.38" +version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" dependencies = [ "android-tzdata", "iana-time-zone", @@ -341,46 +327,46 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.2" +version = "4.5.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a13b88d2c62ff462f88e4a121f17a82c1af05693a2f192b5c38d14de73c19f6" +checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.4.2" +version = "4.5.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bb9faaa7c2ef94b2743a21f5a29e6f0010dff4caa69ac8e9d6cf8b6fa74da08" +checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" dependencies = [ "anstream", "anstyle", "clap_lex", "strsim", - "terminal_size 0.2.6", + "terminal_size 0.4.1", ] [[package]] name = "clap_complete" -version = "4.4.0" +version = "4.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "586a385f7ef2f8b4d86bddaa0c094794e7ccbfe5ffef1f434fe928143fc783a5" +checksum = "ac2e663e3e3bed2d32d065a8404024dad306e699a04263ec59919529f803aee9" dependencies = [ "clap", ] [[package]] name = "clap_lex" -version = "0.5.0" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "clap_mangen" -version = "0.2.9" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0f09a0ca8f0dd8ac92c546b426f466ef19828185c6d504c80c48c9c2768ed9" +checksum = "fbae9cbfdc5d4fa8711c09bd7b83f644cb48281ac35bf97af3e47b0675864bdf" dependencies = [ "clap", "roff", @@ -433,9 +419,9 @@ dependencies = [ [[package]] name = "constant_time_eq" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" [[package]] name = "core-foundation-sys" @@ -597,44 +583,44 @@ dependencies = [ [[package]] name = "cpp" -version = "0.5.9" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa65869ef853e45c60e9828aa08cdd1398cb6e13f3911d9cb2a079b144fcd64" +checksum = "f36bcac3d8234c1fb813358e83d1bb6b0290a3d2b3b5efc6b88bfeaf9d8eec17" dependencies = [ "cpp_macros", ] [[package]] name = "cpp_build" -version = "0.5.9" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e361fae2caf9758164b24da3eedd7f7d7451be30d90d8e7b5d2be29a2f0cf5b" +checksum = "27f8638c97fbd79cc6fc80b616e0e74b49bac21014faed590bbc89b7e2676c90" dependencies = [ "cc", "cpp_common", "lazy_static", "proc-macro2", "regex", - "syn 2.0.86", + "syn 2.0.87", "unicode-xid", ] [[package]] name = "cpp_common" -version = "0.5.9" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e1a2532e4ed4ea13031c13bc7bc0dbca4aae32df48e9d77f0d1e743179f2ea1" +checksum = "25fcfea2ee05889597d35e986c2ad0169694320ae5cc8f6d2640a4bb8a884560" dependencies = [ "lazy_static", "proc-macro2", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] name = "cpp_macros" -version = "0.5.9" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47ec9cc90633446f779ef481a9ce5a0077107dd5b87016440448d908625a83fd" +checksum = "d156158fe86e274820f5a53bc9edb0885a6e7113909497aa8d883b69dd171871" dependencies = [ "aho-corasick", "byteorder", @@ -642,7 +628,7 @@ dependencies = [ "lazy_static", "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -656,9 +642,9 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] @@ -697,22 +683,22 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.19" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "crossterm" -version = "0.27.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ "bitflags 2.6.0", "crossterm_winapi", "filedescriptor", - "libc", - "mio", + "mio 1.0.2", "parking_lot", + "rustix 0.38.40", "signal-hook", "signal-hook-mio", "winapi", @@ -796,7 +782,7 @@ checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -817,13 +803,13 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -912,7 +898,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7199d965852c3bac31f779ef99cbb4537f80e952e2d6aa0ffeb30cce00f4f46e" dependencies = [ "libc", - "thiserror", + "thiserror 1.0.69", "winapi", ] @@ -930,9 +916,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.28" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" dependencies = [ "crc32fast", "miniz_oxide", @@ -961,9 +947,9 @@ dependencies = [ [[package]] name = "fts-sys" -version = "0.2.11" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28ab6a6dfd9184fe8a5097924dea6e7648f499121b3e933bb8486a17f817122e" +checksum = "c427b250eff90452a35afd79fdfcbcf4880e307225bc28bd36d9a2cd78bb6d90" dependencies = [ "bindgen", "libc", @@ -1046,7 +1032,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -1140,9 +1126,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.3.2" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "hex" @@ -1244,6 +1230,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itertools" version = "0.13.0" @@ -1299,15 +1291,15 @@ dependencies = [ [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.161" +version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "libloading" @@ -1359,10 +1351,16 @@ dependencies = [ ] [[package]] -name = "log" -version = "0.4.20" +name = "lockfree-object-pool" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e" + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "lru" @@ -1416,11 +1414,11 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.2" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" dependencies = [ - "adler", + "adler2", ] [[package]] @@ -1435,6 +1433,19 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi", + "libc", + "log", + "wasi", + "windows-sys 0.52.0", +] + [[package]] name = "nix" version = "0.29.0" @@ -1470,7 +1481,7 @@ dependencies = [ "inotify", "kqueue", "libc", - "mio", + "mio 0.8.11", "walkdir", "windows-sys 0.45.0", ] @@ -1544,28 +1555,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", - "libm", -] - -[[package]] -name = "num_enum" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" -dependencies = [ - "num_enum_derive", -] - -[[package]] -name = "num_enum_derive" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.86", ] [[package]] @@ -1722,9 +1711,9 @@ checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" [[package]] name = "platform-info" -version = "2.0.4" +version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91077ffd05d058d70d79eefcd7d7f6aac34980860a7519960f7913b6563a8c3a" +checksum = "7539aeb3fdd8cb4f6a331307cf71a1039cee75e94e8a71725b9484f4a0d9451a" dependencies = [ "libc", "winapi", @@ -1765,7 +1754,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ac2cf0f2e4f42b49f5ffd07dae8d746508ef7526c13940e5f524012ae6c6550" dependencies = [ "proc-macro2", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -1779,9 +1768,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.89" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] @@ -1795,7 +1784,7 @@ dependencies = [ "bitflags 2.6.0", "hex", "procfs-core", - "rustix 0.38.37", + "rustix 0.38.40", ] [[package]] @@ -1808,32 +1797,6 @@ dependencies = [ "hex", ] -[[package]] -name = "proptest" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4c2511913b88df1637da85cc8d96ec8e43a3f8bb8ccb71ee1ac240d6f3df58d" -dependencies = [ - "bit-set", - "bit-vec", - "bitflags 2.6.0", - "lazy_static", - "num-traits", - "rand", - "rand_chacha", - "rand_xorshift", - "regex-syntax", - "rusty-fork", - "tempfile", - "unarray", -] - -[[package]] -name = "quick-error" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" - [[package]] name = "quick-error" version = "2.0.1" @@ -1842,9 +1805,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quote" -version = "1.0.37" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ "proc-macro2", ] @@ -1894,15 +1857,6 @@ dependencies = [ "rand_core", ] -[[package]] -name = "rand_xorshift" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" -dependencies = [ - "rand_core", -] - [[package]] name = "rayon" version = "1.10.0" @@ -2014,7 +1968,7 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn 2.0.86", + "syn 2.0.87", "unicode-ident", ] @@ -2060,9 +2014,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.37" +version = "0.38.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" +checksum = "99e4ea3e1cdc4b559b8e5650f9c8e5998e3e5c1343b4eaf034565f32318d63c0" dependencies = [ "bitflags 2.6.0", "errno", @@ -2071,18 +2025,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "rusty-fork" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" -dependencies = [ - "fnv", - "quick-error 1.2.3", - "tempfile", - "wait-timeout", -] - [[package]] name = "same-file" version = "1.0.6" @@ -2100,9 +2042,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "self_cell" -version = "1.0.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d369a96f978623eb3dc28807c4852d6cc617fed53da5d3c400feff1ef34a714a" +checksum = "c2fdfc24bc566f839a2da4c4295b82db7d25a24253867d5c64355abb5799bdbe" [[package]] name = "selinux" @@ -2115,7 +2057,7 @@ dependencies = [ "once_cell", "reference-counted-singleton", "selinux-sys", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -2138,9 +2080,9 @@ checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4" [[package]] name = "serde" -version = "1.0.214" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" dependencies = [ "serde_derive", ] @@ -2156,13 +2098,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.214" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -2215,12 +2157,12 @@ dependencies = [ [[package]] name = "signal-hook-mio" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" dependencies = [ "libc", - "mio", + "mio 1.0.2", "signal-hook", ] @@ -2233,6 +2175,12 @@ dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "siphasher" version = "0.3.10" @@ -2281,9 +2229,9 @@ dependencies = [ [[package]] name = "strsim" -version = "0.10.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" @@ -2298,9 +2246,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.86" +version = "2.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89275301d38033efb81a6e60e3497e734dfcc62571f2854bf4b16690398824c" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" dependencies = [ "proc-macro2", "quote", @@ -2315,14 +2263,14 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.13.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" +checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" dependencies = [ "cfg-if", "fastrand", "once_cell", - "rustix 0.38.37", + "rustix 0.38.40", "windows-sys 0.59.0", ] @@ -2338,11 +2286,11 @@ dependencies = [ [[package]] name = "terminal_size" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f599bd7ca042cfdf8f4512b277c02ba102247820f9d9d4a9f521f496751a6ef" +checksum = "5352447f921fda68cf61b4101566c0bdb5104eff6804d0678e5227580ab6a4e9" dependencies = [ - "rustix 0.38.37", + "rustix 0.38.40", "windows-sys 0.59.0", ] @@ -2360,29 +2308,49 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.66" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d171f59dbaa811dbbb1aee1e73db92ec2b122911a48e1390dfe327a821ddede" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc" +dependencies = [ + "thiserror-impl 2.0.9", ] [[package]] name = "thiserror-impl" -version = "1.0.66" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b08be0f17bd307950653ce45db00cd31200d82b624b36e181337d9c7d92765b5" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", ] [[package]] name = "time" -version = "0.3.36" +version = "0.3.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" dependencies = [ "deranged", "itoa", @@ -2403,9 +2371,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" dependencies = [ "num-conv", "time-core", @@ -2449,12 +2417,6 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" -[[package]] -name = "unarray" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" - [[package]] name = "unicode-ident" version = "1.0.13" @@ -2511,7 +2473,7 @@ checksum = "e24c654e19afaa6b8f3877ece5d3bed849c2719c56f6752b18ca7da4fcc6e85a" dependencies = [ "cfg-if", "libc", - "thiserror", + "thiserror 1.0.69", "time", "utmp-classic-raw", "zerocopy", @@ -2541,7 +2503,6 @@ name = "uu_base32" version = "0.0.28" dependencies = [ "clap", - "proptest", "uucore", ] @@ -2577,7 +2538,7 @@ version = "0.0.28" dependencies = [ "clap", "nix", - "thiserror", + "thiserror 2.0.9", "uucore", ] @@ -2589,7 +2550,7 @@ dependencies = [ "fts-sys", "libc", "selinux", - "thiserror", + "thiserror 2.0.9", "uucore", ] @@ -2653,7 +2614,7 @@ dependencies = [ "filetime", "indicatif", "libc", - "quick-error 2.0.1", + "quick-error", "selinux", "uucore", "walkdir", @@ -2666,7 +2627,7 @@ version = "0.0.28" dependencies = [ "clap", "regex", - "thiserror", + "thiserror 2.0.9", "uucore", ] @@ -2710,7 +2671,7 @@ version = "0.0.28" dependencies = [ "clap", "tempfile", - "unicode-width 0.1.13", + "unicode-width 0.2.0", "uucore", ] @@ -2773,7 +2734,7 @@ name = "uu_expand" version = "0.0.28" dependencies = [ "clap", - "unicode-width 0.1.13", + "unicode-width 0.2.0", "uucore", ] @@ -2815,7 +2776,7 @@ name = "uu_fmt" version = "0.0.28" dependencies = [ "clap", - "unicode-width 0.1.13", + "unicode-width 0.2.0", "uucore", ] @@ -2951,7 +2912,7 @@ dependencies = [ "number_prefix", "once_cell", "selinux", - "terminal_size 0.4.0", + "terminal_size 0.4.1", "uucore", "uutils_term_grid", ] @@ -3000,7 +2961,7 @@ dependencies = [ "crossterm", "nix", "unicode-segmentation", - "unicode-width 0.1.13", + "unicode-width 0.2.0", "uucore", ] @@ -3101,7 +3062,7 @@ dependencies = [ "chrono", "clap", "itertools", - "quick-error 2.0.1", + "quick-error", "regex", "uucore", ] @@ -3182,7 +3143,7 @@ dependencies = [ "clap", "libc", "selinux", - "thiserror", + "thiserror 2.0.9", "uucore", ] @@ -3243,7 +3204,7 @@ dependencies = [ "rayon", "self_cell", "tempfile", - "unicode-width 0.1.13", + "unicode-width 0.2.0", "uucore", ] @@ -3435,7 +3396,7 @@ name = "uu_unexpand" version = "0.0.28" dependencies = [ "clap", - "unicode-width 0.1.13", + "unicode-width 0.2.0", "uucore", ] @@ -3461,7 +3422,7 @@ version = "0.0.28" dependencies = [ "chrono", "clap", - "thiserror", + "thiserror 2.0.9", "utmp-classic", "uucore", ] @@ -3492,8 +3453,8 @@ dependencies = [ "clap", "libc", "nix", - "thiserror", - "unicode-width 0.1.13", + "thiserror 2.0.9", + "unicode-width 0.2.0", "uucore", ] @@ -3540,6 +3501,7 @@ dependencies = [ "glob", "hex", "itertools", + "lazy_static", "libc", "md-5", "memchr", @@ -3553,7 +3515,7 @@ dependencies = [ "sha3", "sm3", "tempfile", - "thiserror", + "thiserror 2.0.9", "time", "uucore_procs", "walkdir", @@ -3598,15 +3560,6 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" -[[package]] -name = "wait-timeout" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" -dependencies = [ - "libc", -] - [[package]] name = "walkdir" version = "2.5.0" @@ -3644,7 +3597,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", "wasm-bindgen-shared", ] @@ -3666,7 +3619,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3986,7 +3939,7 @@ checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" dependencies = [ "libc", "linux-raw-sys 0.4.14", - "rustix 0.38.37", + "rustix 0.38.40", ] [[package]] @@ -4019,14 +3972,14 @@ checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] name = "zip" -version = "1.1.4" +version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cc23c04387f4da0374be4533ad1208cbb091d5c11d070dfef13676ad6497164" +checksum = "ae9c1ea7b3a5e1f4b922ff856a129881167511563dc219869afe3787fc0c1a45" dependencies = [ "arbitrary", "crc32fast", @@ -4034,6 +3987,21 @@ dependencies = [ "displaydoc", "flate2", "indexmap", - "num_enum", - "thiserror", + "memchr", + "thiserror 2.0.9", + "zopfli", +] + +[[package]] +name = "zopfli" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946" +dependencies = [ + "bumpalo", + "crc32fast", + "lockfree-object-pool", + "log", + "once_cell", + "simd-adler32", ] diff --git a/Cargo.toml b/Cargo.toml index a6881abfb..1991679d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ repository = "https://github.com/uutils/coreutils" readme = "README.md" keywords = ["coreutils", "uutils", "cross-platform", "cli", "utility"] categories = ["command-line-utilities"] -rust-version = "1.70.0" +rust-version = "1.79.0" edition = "2021" build = "build.rs" @@ -276,12 +276,12 @@ chrono = { version = "0.4.38", default-features = false, features = [ "alloc", "clock", ] } -clap = { version = "4.4", features = ["wrap_help", "cargo"] } +clap = { version = "4.5", features = ["wrap_help", "cargo"] } clap_complete = "4.4" clap_mangen = "0.2" compare = "0.1.0" coz = { version = "0.1.3" } -crossterm = ">=0.27.0" +crossterm = "0.28.1" ctrlc = { version = "3.4.4", features = ["termination"] } dns-lookup = { version = "2.0.4" } exacl = "0.12.0" @@ -332,17 +332,17 @@ tempfile = "3.10.1" uutils_term_grid = "0.6" terminal_size = "0.4.0" textwrap = { version = "0.16.1", features = ["terminal_size"] } -thiserror = "1.0.59" +thiserror = "2.0.3" time = { version = "0.3.36" } unicode-segmentation = "1.11.0" -unicode-width = "0.1.12" +unicode-width = "0.2.0" utf-8 = "0.7.6" utmp-classic = "0.1.6" walkdir = "2.5" winapi-util = "0.1.8" windows-sys = { version = "0.59.0", default-features = false } xattr = "1.3.1" -zip = { version = "1.1.4", default-features = false, features = ["deflate"] } +zip = { version = "2.2.2", default-features = false, features = ["deflate"] } hex = "0.4.3" md-5 = "0.10.6" @@ -354,10 +354,10 @@ blake3 = "1.5.1" sm3 = "0.4.2" digest = "0.10.7" -uucore = { version = ">=0.0.19", package = "uucore", path = "src/uucore" } -uucore_procs = { version = ">=0.0.19", package = "uucore_procs", path = "src/uucore_procs" } -uu_ls = { version = ">=0.0.18", path = "src/uu/ls" } -uu_base32 = { version = ">=0.0.18", path = "src/uu/base32" } +uucore = { version = "0.0.28", package = "uucore", path = "src/uucore" } +uucore_procs = { version = "0.0.28", package = "uucore_procs", path = "src/uucore_procs" } +uu_ls = { version = "0.0.28", path = "src/uu/ls" } +uu_base32 = { version = "0.0.28", path = "src/uu/base32" } [dependencies] clap = { workspace = true } diff --git a/GNUmakefile b/GNUmakefile index 0b4f2d04c..af73a10f4 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -147,7 +147,6 @@ UNIX_PROGS := \ nohup \ pathchk \ pinky \ - sleep \ stat \ stdbuf \ timeout \ diff --git a/README.md b/README.md index 22081c689..37c5a596b 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ [![dependency status](https://deps.rs/repo/github/uutils/coreutils/status.svg)](https://deps.rs/repo/github/uutils/coreutils) [![CodeCov](https://codecov.io/gh/uutils/coreutils/branch/master/graph/badge.svg)](https://codecov.io/gh/uutils/coreutils) -![MSRV](https://img.shields.io/badge/MSRV-1.70.0-brightgreen) +![MSRV](https://img.shields.io/badge/MSRV-1.79.0-brightgreen) @@ -70,7 +70,7 @@ the [coreutils docs](https://github.com/uutils/uutils.github.io) repository. ### Rust Version uutils follows Rust's release channels and is tested against stable, beta and -nightly. The current Minimum Supported Rust Version (MSRV) is `1.70.0`. +nightly. The current Minimum Supported Rust Version (MSRV) is `1.79.0`. ## Building diff --git a/deny.toml b/deny.toml index 9fefc7727..d64a2d33a 100644 --- a/deny.toml +++ b/deny.toml @@ -104,6 +104,12 @@ skip = [ { name = "terminal_size", version = "0.2.6" }, # ansi-width, console, os_display { name = "unicode-width", version = "0.1.13" }, + # notify + { name = "mio", version = "0.8.11" }, + # various crates + { name = "thiserror", version = "1.0.69" }, + # thiserror + { name = "thiserror-impl", version = "1.0.69" }, ] # spell-checker: enable diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index a95718bd4..a2bae6dd3 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -546,20 +546,25 @@ dependencies = [ ] [[package]] -name = "libc" -version = "0.2.161" +name = "lazy_static" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.169" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "libfuzzer-sys" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96cfd5557eb82f2b83fed4955246c988d331975a002961b07c81584d107e7f7" +checksum = "9b9569d2f74e257076d8c6bfa73fb505b46b851e51ddaecc825944aa3bed17fa" dependencies = [ "arbitrary", "cc", - "once_cell", ] [[package]] @@ -714,7 +719,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a6229bad892b46b0dcfaaeb18ad0d2e56400f5aaea05b768bde96e73676cf75" dependencies = [ - "unicode-width", + "unicode-width 0.1.12", ] [[package]] @@ -742,9 +747,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.83" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b33eb56c327dec362a9e55b3ad14f9d2f0904fb5a5b03b513ab5465399e9f43" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] @@ -850,9 +855,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.37" +version = "0.38.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" +checksum = "99e4ea3e1cdc4b559b8e5650f9c8e5998e3e5c1343b4eaf034565f32318d63c0" dependencies = [ "bitflags 2.5.0", "errno", @@ -948,20 +953,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "1.0.109" +version = "2.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2863d96a84c6439701d7a38f9de935ec562c8832cc55d1dde0f513b52fad106" +checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e" dependencies = [ "proc-macro2", "quote", @@ -970,9 +964,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.13.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" +checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" dependencies = [ "cfg-if", "fastrand", @@ -993,18 +987,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.61" +version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +checksum = "c006c85c7651b3cf2ada4584faa36773bd07bac24acfb39f3c431b36d7e667aa" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.61" +version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568" dependencies = [ "proc-macro2", "quote", @@ -1044,6 +1038,12 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + [[package]] name = "utf8parse" version = "0.2.1" @@ -1146,7 +1146,7 @@ dependencies = [ "rayon", "self_cell", "tempfile", - "unicode-width", + "unicode-width 0.2.0", "uucore", ] @@ -1186,7 +1186,7 @@ dependencies = [ "libc", "nix 0.29.0", "thiserror", - "unicode-width", + "unicode-width 0.2.0", "uucore", ] @@ -1204,6 +1204,7 @@ dependencies = [ "glob", "hex", "itertools", + "lazy_static", "libc", "md-5", "memchr", diff --git a/src/bin/uudoc.rs b/src/bin/uudoc.rs index 8ea11ed4d..f2c325e32 100644 --- a/src/bin/uudoc.rs +++ b/src/bin/uudoc.rs @@ -172,7 +172,7 @@ struct MDWriter<'a, 'b> { markdown: Option, } -impl<'a, 'b> MDWriter<'a, 'b> { +impl MDWriter<'_, '_> { /// # Errors /// Returns an error if the writer fails. fn markdown(&mut self) -> io::Result<()> { diff --git a/src/uu/base32/Cargo.toml b/src/uu/base32/Cargo.toml index ffcd4796c..26ab2bc6f 100644 --- a/src/uu/base32/Cargo.toml +++ b/src/uu/base32/Cargo.toml @@ -1,5 +1,3 @@ -# spell-checker:ignore proptest - [package] name = "uu_base32" version = "0.0.28" @@ -22,9 +20,6 @@ path = "src/base32.rs" clap = { workspace = true } uucore = { workspace = true, features = ["encoding"] } -[dev-dependencies] -proptest = "1.5.0" - [[bin]] name = "base32" path = "src/main.rs" diff --git a/src/uu/base32/src/base32.rs b/src/uu/base32/src/base32.rs index 46a0361ea..e14e83921 100644 --- a/src/uu/base32/src/base32.rs +++ b/src/uu/base32/src/base32.rs @@ -5,6 +5,7 @@ pub mod base_common; +use base_common::ReadSeek; use clap::Command; use uucore::{encoding::Format, error::UResult, help_about, help_usage}; @@ -17,7 +18,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let config = base_common::parse_base_cmd_args(args, ABOUT, USAGE)?; - let mut input = base_common::get_input(&config)?; + let mut input: Box = base_common::get_input(&config)?; base_common::handle_input(&mut input, format, config) } diff --git a/src/uu/base32/src/base_common.rs b/src/uu/base32/src/base_common.rs index f6b88f551..878d07a92 100644 --- a/src/uu/base32/src/base_common.rs +++ b/src/uu/base32/src/base_common.rs @@ -3,15 +3,15 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore hexupper lsbf msbf unpadded +// spell-checker:ignore hexupper lsbf msbf unpadded nopad aGVsbG8sIHdvcmxkIQ use clap::{crate_version, Arg, ArgAction, Command}; use std::fs::File; -use std::io::{self, ErrorKind, Read}; +use std::io::{self, ErrorKind, Read, Seek, SeekFrom}; use std::path::{Path, PathBuf}; use uucore::display::Quotable; use uucore::encoding::{ - for_base_common::{BASE32, BASE32HEX, BASE64, BASE64URL, HEXUPPER}, + for_base_common::{BASE32, BASE32HEX, BASE64, BASE64URL, BASE64_NOPAD, HEXUPPER_PERMISSIVE}, Format, Z85Wrapper, BASE2LSBF, BASE2MSBF, }; use uucore::encoding::{EncodingWrapper, SupportsFastDecodeAndEncode}; @@ -143,25 +143,50 @@ pub fn base_app(about: &'static str, usage: &str) -> Command { ) } -pub fn get_input(config: &Config) -> UResult> { +/// A trait alias for types that implement both `Read` and `Seek`. +pub trait ReadSeek: Read + Seek {} + +/// Automatically implement the `ReadSeek` trait for any type that implements both `Read` and `Seek`. +impl ReadSeek for T {} + +pub fn get_input(config: &Config) -> UResult> { match &config.to_read { Some(path_buf) => { // Do not buffer input, because buffering is handled by `fast_decode` and `fast_encode` let file = File::open(path_buf).map_err_context(|| path_buf.maybe_quote().to_string())?; - Ok(Box::new(file)) } None => { - let stdin_lock = io::stdin().lock(); - - Ok(Box::new(stdin_lock)) + let mut buffer = Vec::new(); + io::stdin().read_to_end(&mut buffer)?; + Ok(Box::new(io::Cursor::new(buffer))) } } } -pub fn handle_input(input: &mut R, format: Format, config: Config) -> UResult<()> { - let supports_fast_decode_and_encode = get_supports_fast_decode_and_encode(format); +/// Determines if the input buffer ends with padding ('=') after trimming trailing whitespace. +fn has_padding(input: &mut R) -> UResult { + let mut buf = Vec::new(); + input + .read_to_end(&mut buf) + .map_err(|err| USimpleError::new(1, format_read_error(err.kind())))?; + + // Reverse iterator and skip trailing whitespace without extra collections + let has_padding = buf + .iter() + .rfind(|&&byte| !byte.is_ascii_whitespace()) + .is_some_and(|&byte| byte == b'='); + + input.seek(SeekFrom::Start(0))?; + Ok(has_padding) +} + +pub fn handle_input(input: &mut R, format: Format, config: Config) -> UResult<()> { + let has_padding = has_padding(input)?; + + let supports_fast_decode_and_encode = + get_supports_fast_decode_and_encode(format, config.decode, has_padding); let supports_fast_decode_and_encode_ref = supports_fast_decode_and_encode.as_ref(); @@ -184,7 +209,11 @@ pub fn handle_input(input: &mut R, format: Format, config: Config) -> U } } -pub fn get_supports_fast_decode_and_encode(format: Format) -> Box { +pub fn get_supports_fast_decode_and_encode( + format: Format, + decode: bool, + has_padding: bool, +) -> Box { const BASE16_VALID_DECODING_MULTIPLE: usize = 2; const BASE2_VALID_DECODING_MULTIPLE: usize = 8; const BASE32_VALID_DECODING_MULTIPLE: usize = 8; @@ -197,11 +226,11 @@ pub fn get_supports_fast_decode_and_encode(format: Format) -> Box Box::from(EncodingWrapper::new( - HEXUPPER, + HEXUPPER_PERMISSIVE, BASE16_VALID_DECODING_MULTIPLE, BASE16_UNPADDED_MULTIPLE, // spell-checker:disable-next-line - b"0123456789ABCDEF", + b"0123456789ABCDEFabcdef", )), Format::Base2Lsbf => Box::from(EncodingWrapper::new( BASE2LSBF, @@ -231,13 +260,24 @@ pub fn get_supports_fast_decode_and_encode(format: Format) -> Box { + let alphabet: &[u8] = if has_padding { + &b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/="[..] + } else { + &b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/"[..] + }; + let wrapper = if decode && !has_padding { + BASE64_NOPAD + } else { + BASE64 + }; + Box::from(EncodingWrapper::new( + wrapper, + BASE64_VALID_DECODING_MULTIPLE, + BASE64_UNPADDED_MULTIPLE, + alphabet, + )) + } Format::Base64Url => Box::from(EncodingWrapper::new( BASE64URL, BASE64_VALID_DECODING_MULTIPLE, @@ -316,6 +356,7 @@ pub mod fast_encode { encoded_buffer: &mut VecDeque, output: &mut dyn Write, is_cleanup: bool, + empty_wrap: bool, ) -> io::Result<()> { // TODO // `encoded_buffer` only has to be a VecDeque if line wrapping is enabled @@ -324,7 +365,9 @@ pub mod fast_encode { output.write_all(encoded_buffer.make_contiguous())?; if is_cleanup { - output.write_all(b"\n")?; + if !empty_wrap { + output.write_all(b"\n")?; + } } else { encoded_buffer.clear(); } @@ -377,25 +420,26 @@ pub mod fast_encode { } fn write_to_output( - line_wrapping_option: &mut Option, + line_wrapping: &mut Option, encoded_buffer: &mut VecDeque, output: &mut dyn Write, is_cleanup: bool, + empty_wrap: bool, ) -> io::Result<()> { // Write all data in `encoded_buffer` to `output` - if let &mut Some(ref mut li) = line_wrapping_option { + if let &mut Some(ref mut li) = line_wrapping { write_with_line_breaks(li, encoded_buffer, output, is_cleanup)?; } else { - write_without_line_breaks(encoded_buffer, output, is_cleanup)?; + write_without_line_breaks(encoded_buffer, output, is_cleanup, empty_wrap)?; } Ok(()) } // End of helper functions - pub fn fast_encode( - input: &mut R, - mut output: W, + pub fn fast_encode( + input: &mut dyn Read, + output: &mut dyn Write, supports_fast_decode_and_encode: &dyn SupportsFastDecodeAndEncode, wrap: Option, ) -> UResult<()> { @@ -473,16 +517,21 @@ pub mod fast_encode { )?; assert!(leftover_buffer.len() < encode_in_chunks_of_size); - // Write all data in `encoded_buffer` to `output` - write_to_output(&mut line_wrapping, &mut encoded_buffer, &mut output, false)?; + write_to_output( + &mut line_wrapping, + &mut encoded_buffer, + output, + false, + wrap == Some(0), + )?; } Err(er) => { let kind = er.kind(); if kind == ErrorKind::Interrupted { - // TODO - // Retry reading? + // Retry reading + continue; } return Err(USimpleError::new(1, format_read_error(kind))); @@ -499,7 +548,13 @@ pub mod fast_encode { // Write all data in `encoded_buffer` to output // `is_cleanup` triggers special cleanup-only logic - write_to_output(&mut line_wrapping, &mut encoded_buffer, &mut output, true)?; + write_to_output( + &mut line_wrapping, + &mut encoded_buffer, + output, + true, + wrap == Some(0), + )?; } Ok(()) @@ -606,9 +661,9 @@ pub mod fast_decode { } // End of helper functions - pub fn fast_decode( - input: &mut R, - mut output: &mut W, + pub fn fast_decode( + input: &mut dyn Read, + output: &mut dyn Write, supports_fast_decode_and_encode: &dyn SupportsFastDecodeAndEncode, ignore_garbage: bool, ) -> UResult<()> { @@ -711,14 +766,14 @@ pub mod fast_decode { assert!(leftover_buffer.len() < decode_in_chunks_of_size); // Write all data in `decoded_buffer` to `output` - write_to_output(&mut decoded_buffer, &mut output)?; + write_to_output(&mut decoded_buffer, output)?; } Err(er) => { let kind = er.kind(); if kind == ErrorKind::Interrupted { - // TODO - // Retry reading? + // Retry reading + continue; } return Err(USimpleError::new(1, format_read_error(kind))); @@ -734,7 +789,7 @@ pub mod fast_decode { .decode_into_vec(&leftover_buffer, &mut decoded_buffer)?; // Write all data in `decoded_buffer` to `output` - write_to_output(&mut decoded_buffer, &mut output)?; + write_to_output(&mut decoded_buffer, output)?; } Ok(()) @@ -759,3 +814,33 @@ fn format_read_error(kind: ErrorKind) -> String { format!("read error: {kind_string_capitalized}") } + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Cursor; + + #[test] + fn test_has_padding() { + let test_cases = vec![ + ("aGVsbG8sIHdvcmxkIQ==", true), + ("aGVsbG8sIHdvcmxkIQ== ", true), + ("aGVsbG8sIHdvcmxkIQ==\n", true), + ("aGVsbG8sIHdvcmxkIQ== \n", true), + ("aGVsbG8sIHdvcmxkIQ=", true), + ("aGVsbG8sIHdvcmxkIQ= ", true), + ("aGVsbG8sIHdvcmxkIQ \n", false), + ("aGVsbG8sIHdvcmxkIQ", false), + ]; + + for (input, expected) in test_cases { + let mut cursor = Cursor::new(input.as_bytes()); + assert_eq!( + has_padding(&mut cursor).unwrap(), + expected, + "Failed for input: '{}'", + input + ); + } + } +} diff --git a/src/uu/base32/tests/property_tests.rs b/src/uu/base32/tests/property_tests.rs deleted file mode 100644 index 0f2393c42..000000000 --- a/src/uu/base32/tests/property_tests.rs +++ /dev/null @@ -1,430 +0,0 @@ -// spell-checker:ignore lsbf msbf proptest - -use proptest::{prelude::TestCaseError, prop_assert, prop_assert_eq, test_runner::TestRunner}; -use std::io::Cursor; -use uu_base32::base_common::{fast_decode, fast_encode, get_supports_fast_decode_and_encode}; -use uucore::encoding::{Format, SupportsFastDecodeAndEncode}; - -const CASES: u32 = { - #[cfg(debug_assertions)] - { - 32 - } - - #[cfg(not(debug_assertions))] - { - 128 - } -}; - -const NORMAL_INPUT_SIZE_LIMIT: usize = { - #[cfg(debug_assertions)] - { - // 256 kibibytes - 256 * 1024 - } - - #[cfg(not(debug_assertions))] - { - // 4 mebibytes - 4 * 1024 * 1024 - } -}; - -const LARGE_INPUT_SIZE_LIMIT: usize = 4 * NORMAL_INPUT_SIZE_LIMIT; - -// Note that `TestRunner`s cannot be reused -fn get_test_runner() -> TestRunner { - TestRunner::new(proptest::test_runner::Config { - cases: CASES, - failure_persistence: None, - - ..proptest::test_runner::Config::default() - }) -} - -fn generic_round_trip(format: Format) { - let supports_fast_decode_and_encode = get_supports_fast_decode_and_encode(format); - - let supports_fast_decode_and_encode_ref = supports_fast_decode_and_encode.as_ref(); - - // Make sure empty inputs round trip - { - get_test_runner() - .run( - &( - proptest::bool::ANY, - proptest::bool::ANY, - proptest::option::of(0_usize..512_usize), - ), - |(ignore_garbage, line_wrap_zero, line_wrap)| { - configurable_round_trip( - format, - supports_fast_decode_and_encode_ref, - ignore_garbage, - line_wrap_zero, - line_wrap, - // Do not add garbage - Vec::<(usize, u8)>::new(), - // Empty input - Vec::::new(), - ) - }, - ) - .unwrap(); - } - - // Unusually large line wrapping settings - { - get_test_runner() - .run( - &( - proptest::bool::ANY, - proptest::bool::ANY, - proptest::option::of(512_usize..65_535_usize), - proptest::collection::vec(proptest::num::u8::ANY, 0..NORMAL_INPUT_SIZE_LIMIT), - ), - |(ignore_garbage, line_wrap_zero, line_wrap, input)| { - configurable_round_trip( - format, - supports_fast_decode_and_encode_ref, - ignore_garbage, - line_wrap_zero, - line_wrap, - // Do not add garbage - Vec::<(usize, u8)>::new(), - input, - ) - }, - ) - .unwrap(); - } - - // Spend more time on sane line wrapping settings - { - get_test_runner() - .run( - &( - proptest::bool::ANY, - proptest::bool::ANY, - proptest::option::of(0_usize..512_usize), - proptest::collection::vec(proptest::num::u8::ANY, 0..NORMAL_INPUT_SIZE_LIMIT), - ), - |(ignore_garbage, line_wrap_zero, line_wrap, input)| { - configurable_round_trip( - format, - supports_fast_decode_and_encode_ref, - ignore_garbage, - line_wrap_zero, - line_wrap, - // Do not add garbage - Vec::<(usize, u8)>::new(), - input, - ) - }, - ) - .unwrap(); - } - - // Test with garbage data - { - get_test_runner() - .run( - &( - proptest::bool::ANY, - proptest::bool::ANY, - proptest::option::of(0_usize..512_usize), - // Garbage data to insert - proptest::collection::vec( - ( - // Random index - proptest::num::usize::ANY, - // In all of the encodings being tested, non-ASCII bytes are garbage - 128_u8..=u8::MAX, - ), - 0..4_096, - ), - proptest::collection::vec(proptest::num::u8::ANY, 0..NORMAL_INPUT_SIZE_LIMIT), - ), - |(ignore_garbage, line_wrap_zero, line_wrap, garbage_data, input)| { - configurable_round_trip( - format, - supports_fast_decode_and_encode_ref, - ignore_garbage, - line_wrap_zero, - line_wrap, - garbage_data, - input, - ) - }, - ) - .unwrap(); - } - - // Test small inputs - { - get_test_runner() - .run( - &( - proptest::bool::ANY, - proptest::bool::ANY, - proptest::option::of(0_usize..512_usize), - proptest::collection::vec(proptest::num::u8::ANY, 0..1_024), - ), - |(ignore_garbage, line_wrap_zero, line_wrap, input)| { - configurable_round_trip( - format, - supports_fast_decode_and_encode_ref, - ignore_garbage, - line_wrap_zero, - line_wrap, - // Do not add garbage - Vec::<(usize, u8)>::new(), - input, - ) - }, - ) - .unwrap(); - } - - // Test small inputs with garbage data - { - get_test_runner() - .run( - &( - proptest::bool::ANY, - proptest::bool::ANY, - proptest::option::of(0_usize..512_usize), - // Garbage data to insert - proptest::collection::vec( - ( - // Random index - proptest::num::usize::ANY, - // In all of the encodings being tested, non-ASCII bytes are garbage - 128_u8..=u8::MAX, - ), - 0..1_024, - ), - proptest::collection::vec(proptest::num::u8::ANY, 0..1_024), - ), - |(ignore_garbage, line_wrap_zero, line_wrap, garbage_data, input)| { - configurable_round_trip( - format, - supports_fast_decode_and_encode_ref, - ignore_garbage, - line_wrap_zero, - line_wrap, - garbage_data, - input, - ) - }, - ) - .unwrap(); - } - - // Test large inputs - { - get_test_runner() - .run( - &( - proptest::bool::ANY, - proptest::bool::ANY, - proptest::option::of(0_usize..512_usize), - proptest::collection::vec(proptest::num::u8::ANY, 0..LARGE_INPUT_SIZE_LIMIT), - ), - |(ignore_garbage, line_wrap_zero, line_wrap, input)| { - configurable_round_trip( - format, - supports_fast_decode_and_encode_ref, - ignore_garbage, - line_wrap_zero, - line_wrap, - // Do not add garbage - Vec::<(usize, u8)>::new(), - input, - ) - }, - ) - .unwrap(); - } -} - -fn configurable_round_trip( - format: Format, - supports_fast_decode_and_encode: &dyn SupportsFastDecodeAndEncode, - ignore_garbage: bool, - line_wrap_zero: bool, - line_wrap: Option, - garbage_data: Vec<(usize, u8)>, - mut input: Vec, -) -> Result<(), TestCaseError> { - // Z85 only accepts inputs with lengths divisible by 4 - if let Format::Z85 = format { - // Reduce length of "input" until it is divisible by 4 - input.truncate((input.len() / 4) * 4); - - assert!((input.len() % 4) == 0); - } - - let line_wrap_to_use = if line_wrap_zero { Some(0) } else { line_wrap }; - - let input_len = input.len(); - - let garbage_data_len = garbage_data.len(); - - let garbage_data_is_empty = garbage_data_len == 0; - - let (input, encoded) = { - let mut output = Vec::with_capacity(input_len * 8); - - let mut cursor = Cursor::new(input); - - fast_encode::fast_encode( - &mut cursor, - &mut output, - supports_fast_decode_and_encode, - line_wrap_to_use, - ) - .unwrap(); - - (cursor.into_inner(), output) - }; - - let encoded_or_encoded_with_garbage = if garbage_data_is_empty { - encoded - } else { - let encoded_len = encoded.len(); - - let encoded_highest_index = match encoded_len.checked_sub(1) { - Some(0) | None => None, - Some(x) => Some(x), - }; - - let mut garbage_data_indexed = vec![Option::::None; encoded_len]; - - let mut encoded_with_garbage = Vec::::with_capacity(encoded_len + garbage_data_len); - - for (index, garbage_byte) in garbage_data { - if let Some(x) = encoded_highest_index { - let index_to_use = index % x; - - garbage_data_indexed[index_to_use] = Some(garbage_byte); - } else { - encoded_with_garbage.push(garbage_byte); - } - } - - for (index, encoded_byte) in encoded.into_iter().enumerate() { - encoded_with_garbage.push(encoded_byte); - - if let Some(garbage_byte) = garbage_data_indexed[index] { - encoded_with_garbage.push(garbage_byte); - } - } - - encoded_with_garbage - }; - - match line_wrap_to_use { - Some(0) => { - let line_endings_count = encoded_or_encoded_with_garbage - .iter() - .filter(|byte| **byte == b'\n') - .count(); - - // If line wrapping is disabled, there should only be one '\n' character (at the very end of the output) - prop_assert_eq!(line_endings_count, 1); - } - _ => { - // TODO - // Validate other line wrapping settings - } - } - - let decoded_or_error = { - let mut output = Vec::with_capacity(input_len); - - let mut cursor = Cursor::new(encoded_or_encoded_with_garbage); - - match fast_decode::fast_decode( - &mut cursor, - &mut output, - supports_fast_decode_and_encode, - ignore_garbage, - ) { - Ok(()) => Ok(output), - Err(er) => Err(er), - } - }; - - let made_round_trip = match decoded_or_error { - Ok(ve) => input.as_slice() == ve.as_slice(), - Err(_) => false, - }; - - let result_was_correct = if garbage_data_is_empty || ignore_garbage { - // If there was no garbage data added, or if "ignore_garbage" was enabled, expect the round trip to succeed - made_round_trip - } else { - // If garbage data was added, and "ignore_garbage" was disabled, expect the round trip to fail - - !made_round_trip - }; - - if !result_was_correct { - eprintln!( - "\ -(configurable_round_trip) FAILURE -format: {format:?} -ignore_garbage: {ignore_garbage} -line_wrap_to_use: {line_wrap_to_use:?} -garbage_data_len: {garbage_data_len} -input_len: {input_len} -", - ); - } - - prop_assert!(result_was_correct); - - Ok(()) -} - -#[test] -fn base16_round_trip() { - generic_round_trip(Format::Base16); -} - -#[test] -fn base2lsbf_round_trip() { - generic_round_trip(Format::Base2Lsbf); -} - -#[test] -fn base2msbf_round_trip() { - generic_round_trip(Format::Base2Msbf); -} - -#[test] -fn base32_round_trip() { - generic_round_trip(Format::Base32); -} - -#[test] -fn base32hex_round_trip() { - generic_round_trip(Format::Base32Hex); -} - -#[test] -fn base64_round_trip() { - generic_round_trip(Format::Base64); -} - -#[test] -fn base64url_round_trip() { - generic_round_trip(Format::Base64Url); -} - -#[test] -fn z85_round_trip() { - generic_round_trip(Format::Z85); -} diff --git a/src/uu/chcon/src/chcon.rs b/src/uu/chcon/src/chcon.rs index 1a804bd3b..b5b892f6c 100644 --- a/src/uu/chcon/src/chcon.rs +++ b/src/uu/chcon/src/chcon.rs @@ -727,7 +727,7 @@ fn get_root_dev_ino() -> Result { } fn root_dev_ino_check(root_dev_ino: Option, dir_dev_ino: DeviceAndINode) -> bool { - root_dev_ino.map_or(false, |root_dev_ino| root_dev_ino == dir_dev_ino) + root_dev_ino == Some(dir_dev_ino) } fn root_dev_ino_warn(dir_name: &Path) { @@ -777,7 +777,7 @@ enum SELinuxSecurityContext<'t> { String(Option), } -impl<'t> SELinuxSecurityContext<'t> { +impl SELinuxSecurityContext<'_> { fn to_c_string(&self) -> Result>> { match self { Self::File(context) => context diff --git a/src/uu/cksum/src/cksum.rs b/src/uu/cksum/src/cksum.rs index e7d73a3bb..e96d2de6f 100644 --- a/src/uu/cksum/src/cksum.rs +++ b/src/uu/cksum/src/cksum.rs @@ -13,8 +13,8 @@ use std::iter; use std::path::Path; use uucore::checksum::{ calculate_blake2b_length, detect_algo, digest_reader, perform_checksum_validation, - ChecksumError, ALGORITHM_OPTIONS_BLAKE2B, ALGORITHM_OPTIONS_BSD, ALGORITHM_OPTIONS_CRC, - ALGORITHM_OPTIONS_SYSV, SUPPORTED_ALGORITHMS, + ChecksumError, ChecksumOptions, ALGORITHM_OPTIONS_BLAKE2B, ALGORITHM_OPTIONS_BSD, + ALGORITHM_OPTIONS_CRC, ALGORITHM_OPTIONS_SYSV, SUPPORTED_ALGORITHMS, }; use uucore::{ encoding, @@ -22,7 +22,7 @@ use uucore::{ format_usage, help_about, help_section, help_usage, line_ending::LineEnding, os_str_as_bytes, show, - sum::{div_ceil, Digest}, + sum::Digest, }; const USAGE: &str = help_usage!("cksum.md"); @@ -124,7 +124,7 @@ where format!( "{} {}{}", sum.parse::().unwrap(), - div_ceil(sz, options.output_bits), + sz.div_ceil(options.output_bits), if not_file { "" } else { " " } ), !not_file, @@ -134,7 +134,7 @@ where format!( "{:0bsd_width$} {:bsd_width$}{}", sum.parse::().unwrap(), - div_ceil(sz, options.output_bits), + sz.div_ceil(options.output_bits), if not_file { "" } else { " " } ), !not_file, @@ -318,17 +318,16 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { || iter::once(OsStr::new("-")).collect::>(), |files| files.map(OsStr::new).collect::>(), ); - return perform_checksum_validation( - files.iter().copied(), - strict, - status, - warn, - binary_flag, + let opts = ChecksumOptions { + binary: binary_flag, ignore_missing, quiet, - algo_option, - length, - ); + status, + strict, + warn, + }; + + return perform_checksum_validation(files.iter().copied(), algo_option, length, opts); } let (tag, asterisk) = handle_tag_text_binary_flags(&matches)?; diff --git a/src/uu/comm/src/comm.rs b/src/uu/comm/src/comm.rs index cae405865..ae57b8bf8 100644 --- a/src/uu/comm/src/comm.rs +++ b/src/uu/comm/src/comm.rs @@ -6,9 +6,8 @@ // spell-checker:ignore (ToDO) delim mkdelim use std::cmp::Ordering; -use std::fs::File; +use std::fs::{metadata, File}; use std::io::{self, stdin, BufRead, BufReader, Stdin}; -use std::path::Path; use uucore::error::{FromIo, UResult, USimpleError}; use uucore::line_ending::LineEnding; use uucore::{format_usage, help_about, help_usage}; @@ -130,7 +129,10 @@ fn open_file(name: &str, line_ending: LineEnding) -> io::Result { if name == "-" { Ok(LineReader::new(Input::Stdin(stdin()), line_ending)) } else { - let f = File::open(Path::new(name))?; + if metadata(name)?.is_dir() { + return Err(io::Error::new(io::ErrorKind::Other, "Is a directory")); + } + let f = File::open(name)?; Ok(LineReader::new( Input::FileIn(BufReader::new(f)), line_ending, diff --git a/src/uu/cp/Cargo.toml b/src/uu/cp/Cargo.toml index 6801e6a09..3912f3308 100644 --- a/src/uu/cp/Cargo.toml +++ b/src/uu/cp/Cargo.toml @@ -30,6 +30,7 @@ uucore = { workspace = true, features = [ "backup-control", "entries", "fs", + "fsxattr", "perms", "mode", "update-control", diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index 32168b090..b74694047 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -17,6 +17,8 @@ use std::os::unix::ffi::OsStrExt; #[cfg(unix)] use std::os::unix::fs::{FileTypeExt, PermissionsExt}; use std::path::{Path, PathBuf, StripPrefixError}; +#[cfg(all(unix, not(target_os = "android")))] +use uucore::fsxattr::copy_xattrs; use clap::{builder::ValueParser, crate_version, Arg, ArgAction, ArgMatches, Command}; use filetime::FileTime; @@ -1605,12 +1607,7 @@ pub(crate) fn copy_attributes( handle_preserve(&attributes.xattr, || -> CopyResult<()> { #[cfg(all(unix, not(target_os = "android")))] { - let xattrs = xattr::list(source)?; - for attr in xattrs { - if let Some(attr_value) = xattr::get(source, attr.clone())? { - xattr::set(dest, attr, &attr_value[..])?; - } - } + copy_xattrs(source, dest)?; } #[cfg(not(all(unix, not(target_os = "android"))))] { diff --git a/src/uu/csplit/src/csplit.rs b/src/uu/csplit/src/csplit.rs index 9e132b704..0602f0dee 100644 --- a/src/uu/csplit/src/csplit.rs +++ b/src/uu/csplit/src/csplit.rs @@ -197,7 +197,7 @@ struct SplitWriter<'a> { dev_null: bool, } -impl<'a> Drop for SplitWriter<'a> { +impl Drop for SplitWriter<'_> { fn drop(&mut self) { if self.options.elide_empty_files && self.size == 0 { let file_name = self.options.split_name.get(self.counter); @@ -206,7 +206,7 @@ impl<'a> Drop for SplitWriter<'a> { } } -impl<'a> SplitWriter<'a> { +impl SplitWriter<'_> { fn new(options: &CsplitOptions) -> SplitWriter { SplitWriter { options, @@ -621,8 +621,9 @@ pub fn uu_app() -> Command { ) .arg( Arg::new(options::QUIET) - .short('s') + .short('q') .long(options::QUIET) + .visible_short_alias('s') .visible_alias("silent") .help("do not print counts of output file sizes") .action(ArgAction::SetTrue), diff --git a/src/uu/csplit/src/patterns.rs b/src/uu/csplit/src/patterns.rs index bd6c4fbfa..edd632d08 100644 --- a/src/uu/csplit/src/patterns.rs +++ b/src/uu/csplit/src/patterns.rs @@ -106,7 +106,7 @@ pub fn get_patterns(args: &[String]) -> Result, CsplitError> { fn extract_patterns(args: &[String]) -> Result, CsplitError> { let mut patterns = Vec::with_capacity(args.len()); let to_match_reg = - Regex::new(r"^(/(?P.+)/|%(?P.+)%)(?P[\+-]\d+)?$").unwrap(); + Regex::new(r"^(/(?P.+)/|%(?P.+)%)(?P[\+-]?\d+)?$").unwrap(); let execute_ntimes_reg = Regex::new(r"^\{(?P\d+)|\*\}$").unwrap(); let mut iter = args.iter().peekable(); @@ -219,14 +219,15 @@ mod tests { "{*}", "/test3.*end$/", "{4}", - "/test4.*end$/+3", - "/test5.*end$/-3", + "/test4.*end$/3", + "/test5.*end$/+3", + "/test6.*end$/-3", ] .into_iter() .map(|v| v.to_string()) .collect(); let patterns = get_patterns(input.as_slice()).unwrap(); - assert_eq!(patterns.len(), 5); + assert_eq!(patterns.len(), 6); match patterns.first() { Some(Pattern::UpToMatch(reg, 0, ExecutePattern::Times(1))) => { let parsed_reg = format!("{reg}"); @@ -256,12 +257,19 @@ mod tests { _ => panic!("expected UpToMatch pattern"), }; match patterns.get(4) { - Some(Pattern::UpToMatch(reg, -3, ExecutePattern::Times(1))) => { + Some(Pattern::UpToMatch(reg, 3, ExecutePattern::Times(1))) => { let parsed_reg = format!("{reg}"); assert_eq!(parsed_reg, "test5.*end$"); } _ => panic!("expected UpToMatch pattern"), }; + match patterns.get(5) { + Some(Pattern::UpToMatch(reg, -3, ExecutePattern::Times(1))) => { + let parsed_reg = format!("{reg}"); + assert_eq!(parsed_reg, "test6.*end$"); + } + _ => panic!("expected UpToMatch pattern"), + }; } #[test] @@ -273,14 +281,15 @@ mod tests { "{*}", "%test3.*end$%", "{4}", - "%test4.*end$%+3", - "%test5.*end$%-3", + "%test4.*end$%3", + "%test5.*end$%+3", + "%test6.*end$%-3", ] .into_iter() .map(|v| v.to_string()) .collect(); let patterns = get_patterns(input.as_slice()).unwrap(); - assert_eq!(patterns.len(), 5); + assert_eq!(patterns.len(), 6); match patterns.first() { Some(Pattern::SkipToMatch(reg, 0, ExecutePattern::Times(1))) => { let parsed_reg = format!("{reg}"); @@ -310,12 +319,19 @@ mod tests { _ => panic!("expected SkipToMatch pattern"), }; match patterns.get(4) { - Some(Pattern::SkipToMatch(reg, -3, ExecutePattern::Times(1))) => { + Some(Pattern::SkipToMatch(reg, 3, ExecutePattern::Times(1))) => { let parsed_reg = format!("{reg}"); assert_eq!(parsed_reg, "test5.*end$"); } _ => panic!("expected SkipToMatch pattern"), }; + match patterns.get(5) { + Some(Pattern::SkipToMatch(reg, -3, ExecutePattern::Times(1))) => { + let parsed_reg = format!("{reg}"); + assert_eq!(parsed_reg, "test6.*end$"); + } + _ => panic!("expected SkipToMatch pattern"), + }; } #[test] diff --git a/src/uu/cut/src/cut.rs b/src/uu/cut/src/cut.rs index cd6eb22d3..5e128425b 100644 --- a/src/uu/cut/src/cut.rs +++ b/src/uu/cut/src/cut.rs @@ -9,7 +9,7 @@ use bstr::io::BufReadExt; use clap::{builder::ValueParser, crate_version, Arg, ArgAction, ArgMatches, Command}; use std::ffi::OsString; use std::fs::File; -use std::io::{stdin, stdout, BufReader, BufWriter, IsTerminal, Read, Write}; +use std::io::{stdin, stdout, BufRead, BufReader, BufWriter, IsTerminal, Read, Write}; use std::path::Path; use uucore::display::Quotable; use uucore::error::{set_exit_code, FromIo, UResult, USimpleError}; @@ -131,8 +131,9 @@ fn cut_fields_explicit_out_delim( if delim_search.peek().is_none() { if !only_delimited { + // Always write the entire line, even if it doesn't end with `newline_char` out.write_all(line)?; - if line[line.len() - 1] != newline_char { + if line.is_empty() || line[line.len() - 1] != newline_char { out.write_all(&[newline_char])?; } } @@ -214,8 +215,9 @@ fn cut_fields_implicit_out_delim( if delim_search.peek().is_none() { if !only_delimited { + // Always write the entire line, even if it doesn't end with `newline_char` out.write_all(line)?; - if line[line.len() - 1] != newline_char { + if line.is_empty() || line[line.len() - 1] != newline_char { out.write_all(&[newline_char])?; } } @@ -265,10 +267,46 @@ fn cut_fields_implicit_out_delim( Ok(()) } +// The input delimiter is identical to `newline_char` +fn cut_fields_newline_char_delim( + reader: R, + ranges: &[Range], + newline_char: u8, + out_delim: &[u8], +) -> UResult<()> { + let buf_in = BufReader::new(reader); + let mut out = stdout_writer(); + + let segments: Vec<_> = buf_in.split(newline_char).filter_map(|x| x.ok()).collect(); + let mut print_delim = false; + + for &Range { low, high } in ranges { + for i in low..=high { + // "- 1" is necessary because fields start from 1 whereas a Vec starts from 0 + if let Some(segment) = segments.get(i - 1) { + if print_delim { + out.write_all(out_delim)?; + } else { + print_delim = true; + } + out.write_all(segment.as_slice())?; + } else { + break; + } + } + } + out.write_all(&[newline_char])?; + Ok(()) +} + fn cut_fields(reader: R, ranges: &[Range], opts: &Options) -> UResult<()> { let newline_char = opts.line_ending.into(); let field_opts = opts.field_opts.as_ref().unwrap(); // it is safe to unwrap() here - field_opts will always be Some() for cut_fields() call match field_opts.delimiter { + Delimiter::Slice(delim) if delim == [newline_char] => { + let out_delim = opts.out_delimiter.unwrap_or(delim); + cut_fields_newline_char_delim(reader, ranges, newline_char, out_delim) + } Delimiter::Slice(delim) => { let matcher = ExactMatcher::new(delim); match opts.out_delimiter { @@ -348,10 +386,7 @@ fn cut_files(mut filenames: Vec, mode: &Mode) { // Get delimiter and output delimiter from `-d`/`--delimiter` and `--output-delimiter` options respectively // Allow either delimiter to have a value that is neither UTF-8 nor ASCII to align with GNU behavior -fn get_delimiters( - matches: &ArgMatches, - delimiter_is_equal: bool, -) -> UResult<(Delimiter, Option<&[u8]>)> { +fn get_delimiters(matches: &ArgMatches) -> UResult<(Delimiter, Option<&[u8]>)> { let whitespace_delimited = matches.get_flag(options::WHITESPACE_DELIMITED); let delim_opt = matches.get_one::(options::DELIMITER); let delim = match delim_opt { @@ -362,12 +397,7 @@ fn get_delimiters( )); } Some(os_string) => { - // GNU's `cut` supports `-d=` to set the delimiter to `=`. - // Clap parsing is limited in this situation, see: - // https://github.com/uutils/coreutils/issues/2424#issuecomment-863825242 - if delimiter_is_equal { - Delimiter::Slice(b"=") - } else if os_string == "''" || os_string.is_empty() { + if os_string == "''" || os_string.is_empty() { // treat `''` as empty delimiter Delimiter::Slice(b"\0") } else { @@ -421,15 +451,26 @@ mod options { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let args = args.collect::>(); + // GNU's `cut` supports `-d=` to set the delimiter to `=`. + // Clap parsing is limited in this situation, see: + // https://github.com/uutils/coreutils/issues/2424#issuecomment-863825242 + let args: Vec = args + .into_iter() + .map(|x| { + if x == "-d=" { + "--delimiter==".into() + } else { + x + } + }) + .collect(); - let delimiter_is_equal = args.contains(&OsString::from("-d=")); // special case let matches = uu_app().try_get_matches_from(args)?; let complement = matches.get_flag(options::COMPLEMENT); let only_delimited = matches.get_flag(options::ONLY_DELIMITED); - let (delimiter, out_delimiter) = get_delimiters(&matches, delimiter_is_equal)?; + let (delimiter, out_delimiter) = get_delimiters(&matches)?; let line_ending = LineEnding::from_zero_flag(matches.get_flag(options::ZERO_TERMINATED)); // Only one, and only one of cutting mode arguments, i.e. `-b`, `-c`, `-f`, diff --git a/src/uu/cut/src/matcher.rs b/src/uu/cut/src/matcher.rs index 953e083b1..bb0c44d5b 100644 --- a/src/uu/cut/src/matcher.rs +++ b/src/uu/cut/src/matcher.rs @@ -23,7 +23,7 @@ impl<'a> ExactMatcher<'a> { } } -impl<'a> Matcher for ExactMatcher<'a> { +impl Matcher for ExactMatcher<'_> { fn next_match(&self, haystack: &[u8]) -> Option<(usize, usize)> { let mut pos = 0usize; loop { diff --git a/src/uu/cut/src/searcher.rs b/src/uu/cut/src/searcher.rs index 21424790e..41c12cf6e 100644 --- a/src/uu/cut/src/searcher.rs +++ b/src/uu/cut/src/searcher.rs @@ -27,7 +27,7 @@ impl<'a, 'b, M: Matcher> Searcher<'a, 'b, M> { // Iterate over field delimiters // Returns (first, last) positions of each sequence, where `haystack[first..last]` // corresponds to the delimiter. -impl<'a, 'b, M: Matcher> Iterator for Searcher<'a, 'b, M> { +impl Iterator for Searcher<'_, '_, M> { type Item = (usize, usize); fn next(&mut self) -> Option { diff --git a/src/uu/date/src/date.rs b/src/uu/date/src/date.rs index 9c7d86564..766e79bd4 100644 --- a/src/uu/date/src/date.rs +++ b/src/uu/date/src/date.rs @@ -103,7 +103,7 @@ enum Iso8601Format { Ns, } -impl<'a> From<&'a str> for Iso8601Format { +impl From<&str> for Iso8601Format { fn from(s: &str) -> Self { match s { HOURS => Self::Hours, @@ -123,7 +123,7 @@ enum Rfc3339Format { Ns, } -impl<'a> From<&'a str> for Rfc3339Format { +impl From<&str> for Rfc3339Format { fn from(s: &str) -> Self { match s { DATE => Self::Date, diff --git a/src/uu/dd/src/dd.rs b/src/uu/dd/src/dd.rs index 24fab1e2f..ca8c2a8b5 100644 --- a/src/uu/dd/src/dd.rs +++ b/src/uu/dd/src/dd.rs @@ -424,7 +424,7 @@ fn make_linux_iflags(iflags: &IFlags) -> Option { } } -impl<'a> Read for Input<'a> { +impl Read for Input<'_> { fn read(&mut self, buf: &mut [u8]) -> io::Result { let mut base_idx = 0; let target_len = buf.len(); @@ -447,7 +447,7 @@ impl<'a> Read for Input<'a> { } } -impl<'a> Input<'a> { +impl Input<'_> { /// Discard the system file cache for the given portion of the input. /// /// `offset` and `len` specify a contiguous portion of the input. @@ -928,7 +928,7 @@ enum BlockWriter<'a> { Unbuffered(Output<'a>), } -impl<'a> BlockWriter<'a> { +impl BlockWriter<'_> { fn discard_cache(&self, offset: libc::off_t, len: libc::off_t) { match self { Self::Unbuffered(o) => o.discard_cache(offset, len), diff --git a/src/uu/dd/src/numbers.rs b/src/uu/dd/src/numbers.rs index 8a6fa5a7a..d0ee2d90b 100644 --- a/src/uu/dd/src/numbers.rs +++ b/src/uu/dd/src/numbers.rs @@ -2,7 +2,8 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -/// Functions for formatting a number as a magnitude and a unit suffix. + +//! Functions for formatting a number as a magnitude and a unit suffix. /// The first ten powers of 1024. const IEC_BASES: [u128; 10] = [ diff --git a/src/uu/df/src/df.rs b/src/uu/df/src/df.rs index 517f8a31f..8ef84a463 100644 --- a/src/uu/df/src/df.rs +++ b/src/uu/df/src/df.rs @@ -311,7 +311,6 @@ fn is_best(previous: &[MountInfo], mi: &MountInfo) -> bool { /// /// Finally, if there are duplicate entries, the one with the shorter /// path is kept. - fn filter_mount_list(vmi: Vec, opt: &Options) -> Vec { let mut result = vec![]; for mi in vmi { @@ -331,7 +330,6 @@ fn filter_mount_list(vmi: Vec, opt: &Options) -> Vec { /// /// `opt` excludes certain filesystems from consideration and allows for the synchronization of filesystems before running; see /// [`Options`] for more information. - fn get_all_filesystems(opt: &Options) -> UResult> { // Run a sync call before any operation if so instructed. if opt.sync { diff --git a/src/uu/dircolors/README.md b/src/uu/dircolors/README.md index ce8aa965f..62944d490 100644 --- a/src/uu/dircolors/README.md +++ b/src/uu/dircolors/README.md @@ -15,4 +15,4 @@ Run the tests: cargo test --features "dircolors" --no-default-features ``` -Edit `/PATH_TO_COREUTILS/src/uu/dircolors/src/colors.rs` until the tests pass. +Edit `/PATH_TO_COREUTILS/src/uu/dircolors/src/dircolors.rs` until the tests pass. diff --git a/src/uu/du/src/du.rs b/src/uu/du/src/du.rs index a35e9f77e..2392497a9 100644 --- a/src/uu/du/src/du.rs +++ b/src/uu/du/src/du.rs @@ -12,7 +12,7 @@ use std::error::Error; use std::fmt::Display; #[cfg(not(windows))] use std::fs::Metadata; -use std::fs::{self, File}; +use std::fs::{self, DirEntry, File}; use std::io::{BufRead, BufReader}; #[cfg(not(windows))] use std::os::unix::fs::MetadataExt; @@ -138,7 +138,11 @@ struct Stat { } impl Stat { - fn new(path: &Path, options: &TraversalOptions) -> std::io::Result { + fn new( + path: &Path, + dir_entry: Option<&DirEntry>, + options: &TraversalOptions, + ) -> std::io::Result { // Determine whether to dereference (follow) the symbolic link let should_dereference = match &options.dereference { Deref::All => true, @@ -149,8 +153,11 @@ impl Stat { let metadata = if should_dereference { // Get metadata, following symbolic links if necessary fs::metadata(path) + } else if let Some(dir_entry) = dir_entry { + // Get metadata directly from the DirEntry, which is faster on Windows + dir_entry.metadata() } else { - // Get metadata without following symbolic links + // Get metadata from the filesystem without following symbolic links fs::symlink_metadata(path) }?; @@ -164,7 +171,7 @@ impl Stat { Ok(Self { path: path.to_path_buf(), is_dir: metadata.is_dir(), - size: if path.is_dir() { 0 } else { metadata.len() }, + size: if metadata.is_dir() { 0 } else { metadata.len() }, blocks: metadata.blocks(), inodes: 1, inode: Some(file_info), @@ -182,7 +189,7 @@ impl Stat { Ok(Self { path: path.to_path_buf(), is_dir: metadata.is_dir(), - size: if path.is_dir() { 0 } else { metadata.len() }, + size: if metadata.is_dir() { 0 } else { metadata.len() }, blocks: size_on_disk / 1024 * 2, inodes: 1, inode: file_info, @@ -319,7 +326,7 @@ fn du( 'file_loop: for f in read { match f { Ok(entry) => { - match Stat::new(&entry.path(), options) { + match Stat::new(&entry.path(), Some(&entry), options) { Ok(this_stat) => { // We have an exclude list for pattern in &options.excludes { @@ -339,14 +346,21 @@ fn du( } if let Some(inode) = this_stat.inode { - if seen_inodes.contains(&inode) { - if options.count_links { + // 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; } + // Skip further processing for this inode continue; } + // Mark this inode as seen seen_inodes.insert(inode); } + if this_stat.is_dir { if options.one_file_system { if let (Some(this_inode), Some(my_inode)) = @@ -519,7 +533,7 @@ impl StatPrinter { if !self .threshold - .map_or(false, |threshold| threshold.should_exclude(size)) + .is_some_and(|threshold| threshold.should_exclude(size)) && self .max_depth .map_or(true, |max_depth| stat_info.depth <= max_depth) @@ -543,9 +557,6 @@ impl StatPrinter { } fn convert_size(&self, size: u64) -> String { - if self.inodes { - return size.to_string(); - } match self.size_format { SizeFormat::HumanDecimal => uucore::format::human::human_readable( size, @@ -555,7 +566,14 @@ impl StatPrinter { size, uucore::format::human::SizeFormat::Binary, ), - SizeFormat::BlockSize(block_size) => div_ceil(size, block_size).to_string(), + SizeFormat::BlockSize(block_size) => { + if self.inodes { + // we ignore block size (-B) with --inodes + size.to_string() + } else { + size.div_ceil(block_size).to_string() + } + } } } @@ -576,13 +594,6 @@ impl StatPrinter { } } -// This can be replaced with u64::div_ceil once it is stabilized. -// This implementation approach is optimized for when `b` is a constant, -// particularly a power of two. -pub fn div_ceil(a: u64, b: u64) -> u64 { - (a + b - 1) / b -} - // Read file paths from the specified file, separated by null characters fn read_files_from(file_name: &str) -> Result, std::io::Error> { let reader: Box = if file_name == "-" { @@ -638,6 +649,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let summarize = matches.get_flag(options::SUMMARIZE); + let count_links = matches.get_flag(options::COUNT_LINKS); + let max_depth = parse_depth( matches .get_one::(options::MAX_DEPTH) @@ -658,15 +671,19 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } read_files_from(file_from)? - } else { - match matches.get_one::(options::FILE) { - Some(_) => matches - .get_many::(options::FILE) - .unwrap() - .map(PathBuf::from) - .collect(), - None => vec![PathBuf::from(".")], + } else if let Some(files) = matches.get_many::(options::FILE) { + let files = files.map(PathBuf::from); + if count_links { + files.collect() + } else { + // Deduplicate while preserving order + let mut seen = std::collections::HashSet::new(); + files + .filter(|path| seen.insert(path.clone())) + .collect::>() } + } else { + vec![PathBuf::from(".")] }; let time = matches.contains_id(options::TIME).then(|| { @@ -708,7 +725,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } else { Deref::None }, - count_links: matches.get_flag(options::COUNT_LINKS), + count_links, verbose: matches.get_flag(options::VERBOSE), excludes: build_exclude_patterns(&matches)?, }; @@ -765,7 +782,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } // Check existence of path provided in argument - if let Ok(stat) = Stat::new(&path, &traversal_options) { + if let Ok(stat) = Stat::new(&path, None, &traversal_options) { // Kick off the computation of disk usage from the initial path let mut seen_inodes: HashSet = HashSet::new(); if let Some(inode) = stat.inode { diff --git a/src/uu/echo/src/echo.rs b/src/uu/echo/src/echo.rs index 746cdd7c5..097e4f2e9 100644 --- a/src/uu/echo/src/echo.rs +++ b/src/uu/echo/src/echo.rs @@ -208,13 +208,6 @@ fn print_escaped(input: &[u8], output: &mut StdoutLock) -> io::Result= 1.79.0 - // https://github.com/rust-lang/rust/pull/121346 - // TODO: when we have a MSRV >= 1.79.0, delete these "hold" bindings - let hold_one_byte_outside_of_match: [u8; 1_usize]; - let hold_two_bytes_outside_of_match: [u8; 2_usize]; - let unescaped: &[u8] = match *next { b'\\' => br"\", b'a' => b"\x07", @@ -230,12 +223,7 @@ fn print_escaped(input: &[u8], output: &mut StdoutLock) -> io::Result= 1.79.0 - hold_one_byte_outside_of_match = [parsed_hexadecimal_number]; - - // TODO: when we have a MSRV >= 1.79.0, return reference to a temporary array: - // &[parsed_hexadecimal_number] - &hold_one_byte_outside_of_match + &[parsed_hexadecimal_number] } else { // "\x" with any non-hexadecimal digit after means "\x" is treated literally br"\x" @@ -246,12 +234,7 @@ fn print_escaped(input: &[u8], output: &mut StdoutLock) -> io::Result= 1.79.0 - hold_one_byte_outside_of_match = [parsed_octal_number]; - - // TODO: when we have a MSRV >= 1.79.0, return reference to a temporary array: - // &[parsed_octal_number] - &hold_one_byte_outside_of_match + &[parsed_octal_number] } else { // "\0" with any non-octal digit after it means "\0" is treated as ASCII '\0' (NUL), 0x00 b"\0" @@ -259,9 +242,7 @@ fn print_escaped(input: &[u8], output: &mut StdoutLock) -> io::Result { // Backslash and the following byte are treated literally - hold_two_bytes_outside_of_match = [b'\\', other_byte]; - - &hold_two_bytes_outside_of_match + &[b'\\', other_byte] } }; @@ -274,9 +255,26 @@ fn print_escaped(input: &[u8], output: &mut StdoutLock) -> io::Result impl uucore::Args { + let mut result = Vec::new(); + let mut is_first_double_hyphen = true; + + for arg in args { + if arg == "--" && is_first_double_hyphen { + result.push(OsString::from("--")); + is_first_double_hyphen = false; + } + result.push(arg); + } + + result.into_iter() +} + #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().get_matches_from(args); + let matches = uu_app().get_matches_from(handle_double_hyphens(args)); // TODO // "If the POSIXLY_CORRECT environment variable is set, then when echo’s first argument is not -n it outputs option-like arguments instead of treating them as options." diff --git a/src/uu/env/src/string_parser.rs b/src/uu/env/src/string_parser.rs index 0ea4a3c0c..5cc8d77a1 100644 --- a/src/uu/env/src/string_parser.rs +++ b/src/uu/env/src/string_parser.rs @@ -114,10 +114,9 @@ impl<'a> StringParser<'a> { } pub fn peek_chunk(&self) -> Option> { - return self - .get_chunk_with_length_at(self.pointer) + self.get_chunk_with_length_at(self.pointer) .ok() - .map(|(chunk, _)| chunk); + .map(|(chunk, _)| chunk) } pub fn consume_chunk(&mut self) -> Result, Error> { diff --git a/src/uu/env/src/variable_parser.rs b/src/uu/env/src/variable_parser.rs index f225d4945..d08c9f0dc 100644 --- a/src/uu/env/src/variable_parser.rs +++ b/src/uu/env/src/variable_parser.rs @@ -11,7 +11,7 @@ pub struct VariableParser<'a, 'b> { pub parser: &'b mut StringParser<'a>, } -impl<'a, 'b> VariableParser<'a, 'b> { +impl<'a> VariableParser<'a, '_> { fn get_current_char(&self) -> Option { self.parser.peek().ok() } diff --git a/src/uu/fmt/src/fmt.rs b/src/uu/fmt/src/fmt.rs index 007e75dd6..bb2e1a978 100644 --- a/src/uu/fmt/src/fmt.rs +++ b/src/uu/fmt/src/fmt.rs @@ -189,6 +189,13 @@ fn process_file( _ => { let f = File::open(file_name) .map_err_context(|| format!("cannot open {} for reading", file_name.quote()))?; + if f.metadata() + .map_err_context(|| format!("cannot get metadata for {}", file_name.quote()))? + .is_dir() + { + return Err(USimpleError::new(1, "read error".to_string())); + } + Box::new(f) as Box } }); diff --git a/src/uu/fmt/src/linebreak.rs b/src/uu/fmt/src/linebreak.rs index aa1477eba..05d01d1a3 100644 --- a/src/uu/fmt/src/linebreak.rs +++ b/src/uu/fmt/src/linebreak.rs @@ -20,7 +20,7 @@ struct BreakArgs<'a> { ostream: &'a mut BufWriter, } -impl<'a> BreakArgs<'a> { +impl BreakArgs<'_> { fn compute_width(&self, winfo: &WordInfo, posn: usize, fresh: bool) -> usize { if fresh { 0 diff --git a/src/uu/fmt/src/parasplit.rs b/src/uu/fmt/src/parasplit.rs index 1ae8ea34f..8aa18c4c9 100644 --- a/src/uu/fmt/src/parasplit.rs +++ b/src/uu/fmt/src/parasplit.rs @@ -73,7 +73,7 @@ pub struct FileLines<'a> { lines: Lines<&'a mut FileOrStdReader>, } -impl<'a> FileLines<'a> { +impl FileLines<'_> { fn new<'b>(opts: &'b FmtOptions, lines: Lines<&'b mut FileOrStdReader>) -> FileLines<'b> { FileLines { opts, lines } } @@ -144,7 +144,7 @@ impl<'a> FileLines<'a> { } } -impl<'a> Iterator for FileLines<'a> { +impl Iterator for FileLines<'_> { type Item = Line; fn next(&mut self) -> Option { @@ -232,7 +232,7 @@ pub struct ParagraphStream<'a> { opts: &'a FmtOptions, } -impl<'a> ParagraphStream<'a> { +impl ParagraphStream<'_> { pub fn new<'b>(opts: &'b FmtOptions, reader: &'b mut FileOrStdReader) -> ParagraphStream<'b> { let lines = FileLines::new(opts, reader.lines()).peekable(); // at the beginning of the file, we might find mail headers @@ -273,7 +273,7 @@ impl<'a> ParagraphStream<'a> { } } -impl<'a> Iterator for ParagraphStream<'a> { +impl Iterator for ParagraphStream<'_> { type Item = Result; #[allow(clippy::cognitive_complexity)] @@ -491,7 +491,7 @@ struct WordSplit<'a> { prev_punct: bool, } -impl<'a> WordSplit<'a> { +impl WordSplit<'_> { fn analyze_tabs(&self, string: &str) -> (Option, usize, Option) { // given a string, determine (length before tab) and (printed length after first tab) // if there are no tabs, beforetab = -1 and aftertab is the printed length @@ -517,7 +517,7 @@ impl<'a> WordSplit<'a> { } } -impl<'a> WordSplit<'a> { +impl WordSplit<'_> { fn new<'b>(opts: &'b FmtOptions, string: &'b str) -> WordSplit<'b> { // wordsplits *must* start at a non-whitespace character let trim_string = string.trim_start(); diff --git a/src/uu/fold/src/fold.rs b/src/uu/fold/src/fold.rs index 0223248be..e17ba21c3 100644 --- a/src/uu/fold/src/fold.rs +++ b/src/uu/fold/src/fold.rs @@ -99,7 +99,7 @@ pub fn uu_app() -> Command { fn handle_obsolete(args: &[String]) -> (Vec, Option) { for (i, arg) in args.iter().enumerate() { let slice = &arg; - if slice.starts_with('-') && slice.chars().nth(1).map_or(false, |c| c.is_ascii_digit()) { + if slice.starts_with('-') && slice.chars().nth(1).is_some_and(|c| c.is_ascii_digit()) { let mut v = args.to_vec(); v.remove(i); return (v, Some(slice[1..].to_owned())); diff --git a/src/uu/hashsum/src/hashsum.rs b/src/uu/hashsum/src/hashsum.rs index 90c8c8adf..1d3a758f5 100644 --- a/src/uu/hashsum/src/hashsum.rs +++ b/src/uu/hashsum/src/hashsum.rs @@ -23,6 +23,7 @@ use uucore::checksum::digest_reader; use uucore::checksum::escape_filename; use uucore::checksum::perform_checksum_validation; use uucore::checksum::ChecksumError; +use uucore::checksum::ChecksumOptions; use uucore::checksum::HashAlgorithm; use uucore::error::{FromIo, UResult}; use uucore::sum::{Digest, Sha3_224, Sha3_256, Sha3_384, Sha3_512, Shake128, Shake256}; @@ -239,18 +240,21 @@ pub fn uumain(mut args: impl uucore::Args) -> UResult<()> { || iter::once(OsStr::new("-")).collect::>(), |files| files.map(OsStr::new).collect::>(), ); + let opts = ChecksumOptions { + binary, + ignore_missing, + quiet, + status, + strict, + warn, + }; // Execute the checksum validation return perform_checksum_validation( input.iter().copied(), - strict, - status, - warn, - binary, - ignore_missing, - quiet, Some(algo.name), Some(algo.bits), + opts, ); } else if quiet { return Err(ChecksumError::QuietNotCheck.into()); diff --git a/src/uu/join/src/join.rs b/src/uu/join/src/join.rs index e7bc7da69..f01f75b71 100644 --- a/src/uu/join/src/join.rs +++ b/src/uu/join/src/join.rs @@ -109,7 +109,7 @@ struct MultiByteSep<'a> { finder: Finder<'a>, } -impl<'a> Separator for MultiByteSep<'a> { +impl Separator for MultiByteSep<'_> { fn field_ranges(&self, haystack: &[u8], len_guess: usize) -> Vec<(usize, usize)> { let mut field_ranges = Vec::with_capacity(len_guess); let mut last_end = 0; diff --git a/src/uu/ls/src/colors.rs b/src/uu/ls/src/colors.rs index 6c580d18a..4f97e42e2 100644 --- a/src/uu/ls/src/colors.rs +++ b/src/uu/ls/src/colors.rs @@ -156,6 +156,26 @@ pub(crate) fn color_name( target_symlink: Option<&PathData>, wrap: bool, ) -> String { + // Check if the file has capabilities + #[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] + { + // Skip checking capabilities if LS_COLORS=ca=: + let capabilities = style_manager + .colors + .style_for_indicator(Indicator::Capabilities); + + let has_capabilities = if capabilities.is_none() { + false + } else { + uucore::fsxattr::has_acl(path.p_buf.as_path()) + }; + + // If the file has capabilities, use a specific style for `ca` (capabilities) + if has_capabilities { + return style_manager.apply_style(capabilities, name, wrap); + } + } + if !path.must_dereference { // If we need to dereference (follow) a symlink, we will need to get the metadata if let Some(de) = &path.de { diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index f4e347147..9a22006e0 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -21,7 +21,7 @@ use std::os::windows::fs::MetadataExt; use std::{ cmp::Reverse, error::Error, - ffi::OsString, + ffi::{OsStr, OsString}, fmt::{Display, Write as FmtWrite}, fs::{self, DirEntry, FileType, Metadata, ReadDir}, io::{stdout, BufWriter, ErrorKind, Stdout, Write}, @@ -55,7 +55,7 @@ use uucore::libc::{dev_t, major, minor}; #[cfg(unix)] use uucore::libc::{S_IXGRP, S_IXOTH, S_IXUSR}; use uucore::line_ending::LineEnding; -use uucore::quoting_style::{escape_dir_name, escape_name, QuotingStyle}; +use uucore::quoting_style::{self, QuotingStyle}; use uucore::{ display::Quotable, error::{set_exit_code, UError, UResult}, @@ -2048,7 +2048,11 @@ impl PathData { /// file11 /// ``` fn show_dir_name(path_data: &PathData, out: &mut BufWriter, config: &Config) { - let escaped_name = escape_dir_name(path_data.p_buf.as_os_str(), &config.quoting_style); + // FIXME: replace this with appropriate behavior for literal unprintable bytes + let escaped_name = + quoting_style::escape_dir_name(path_data.p_buf.as_os_str(), &config.quoting_style) + .to_string_lossy() + .to_string(); let name = if config.hyperlink && !config.dired { create_hyperlink(&escaped_name, path_data) @@ -3002,7 +3006,6 @@ use std::sync::Mutex; #[cfg(unix)] use uucore::entries; use uucore::fs::FileInformation; -use uucore::quoting_style; #[cfg(unix)] fn cached_uid2usr(uid: u32) -> String { @@ -3542,3 +3545,10 @@ fn calculate_padding_collection( padding_collections } + +// FIXME: replace this with appropriate behavior for literal unprintable bytes +fn escape_name(name: &OsStr, style: &QuotingStyle) -> String { + quoting_style::escape_name(name, style) + .to_string_lossy() + .to_string() +} diff --git a/src/uu/mkfifo/Cargo.toml b/src/uu/mkfifo/Cargo.toml index 960ed601d..68f16a1f6 100644 --- a/src/uu/mkfifo/Cargo.toml +++ b/src/uu/mkfifo/Cargo.toml @@ -19,7 +19,7 @@ path = "src/mkfifo.rs" [dependencies] clap = { workspace = true } libc = { workspace = true } -uucore = { workspace = true } +uucore = { workspace = true, features = ["fs"] } [[bin]] name = "mkfifo" diff --git a/src/uu/mkfifo/src/mkfifo.rs b/src/uu/mkfifo/src/mkfifo.rs index 9320f76ed..01fc5dc1e 100644 --- a/src/uu/mkfifo/src/mkfifo.rs +++ b/src/uu/mkfifo/src/mkfifo.rs @@ -6,6 +6,8 @@ use clap::{crate_version, Arg, ArgAction, Command}; use libc::mkfifo; use std::ffi::CString; +use std::fs; +use std::os::unix::fs::PermissionsExt; use uucore::display::Quotable; use uucore::error::{UResult, USimpleError}; use uucore::{format_usage, help_about, help_usage, show}; @@ -32,11 +34,13 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } let mode = match matches.get_one::(options::MODE) { + // if mode is passed, ignore umask Some(m) => match usize::from_str_radix(m, 8) { Ok(m) => m, Err(e) => return Err(USimpleError::new(1, format!("invalid mode: {e}"))), }, - None => 0o666, + // Default value + umask if present + None => 0o666 & !(uucore::mode::get_umask() as usize), }; let fifos: Vec = match matches.get_many::(options::FIFO) { @@ -47,12 +51,20 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { for f in fifos { let err = unsafe { let name = CString::new(f.as_bytes()).unwrap(); - mkfifo(name.as_ptr(), mode as libc::mode_t) + mkfifo(name.as_ptr(), 0o666) }; if err == -1 { show!(USimpleError::new( 1, - format!("cannot create fifo {}: File exists", f.quote()) + format!("cannot create fifo {}: File exists", f.quote()), + )); + } + + // Explicitly set the permissions to ignore umask + if let Err(e) = fs::set_permissions(&f, fs::Permissions::from_mode(mode as u32)) { + return Err(USimpleError::new( + 1, + format!("cannot set permissions on {}: {}", f.quote(), e), )); } } @@ -71,7 +83,6 @@ pub fn uu_app() -> Command { .short('m') .long(options::MODE) .help("file permissions for the fifo") - .default_value("0666") .value_name("MODE"), ) .arg( diff --git a/src/uu/more/src/more.rs b/src/uu/more/src/more.rs index 0b8c838f2..61d9b2adb 100644 --- a/src/uu/more/src/more.rs +++ b/src/uu/more/src/more.rs @@ -98,10 +98,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { println!("{panic_info}"); })); - let matches = match uu_app().try_get_matches_from(args) { - Ok(m) => m, - Err(e) => return Err(e.into()), - }; + let matches = uu_app().try_get_matches_from(args)?; let mut options = Options::from(&matches); @@ -308,12 +305,12 @@ fn more( rows = number; } - let lines = break_buff(buff, usize::from(cols)); + let lines = break_buff(buff, cols as usize); let mut pager = Pager::new(rows, lines, next_file, options); - if options.pattern.is_some() { - match search_pattern_in_file(&pager.lines, &options.pattern) { + if let Some(pat) = options.pattern.as_ref() { + match search_pattern_in_file(&pager.lines, pat) { Some(number) => pager.upper_mark = number, None => { execute!(stdout, terminal::Clear(terminal::ClearType::CurrentLine))?; @@ -446,8 +443,8 @@ struct Pager<'a> { // The current line at the top of the screen upper_mark: usize, // The number of rows that fit on the screen - content_rows: u16, - lines: Vec, + content_rows: usize, + lines: Vec<&'a str>, next_file: Option<&'a str>, line_count: usize, silent: bool, @@ -456,11 +453,11 @@ struct Pager<'a> { } impl<'a> Pager<'a> { - fn new(rows: u16, lines: Vec, next_file: Option<&'a str>, options: &Options) -> Self { + fn new(rows: u16, lines: Vec<&'a str>, next_file: Option<&'a str>, options: &Options) -> Self { let line_count = lines.len(); Self { upper_mark: options.from_line, - content_rows: rows.saturating_sub(1), + content_rows: rows.saturating_sub(1) as usize, lines, next_file, line_count, @@ -472,30 +469,25 @@ impl<'a> Pager<'a> { fn should_close(&mut self) -> bool { self.upper_mark - .saturating_add(self.content_rows.into()) + .saturating_add(self.content_rows) .ge(&self.line_count) } fn page_down(&mut self) { // If the next page down position __after redraw__ is greater than the total line count, // the upper mark must not grow past top of the screen at the end of the open file. - if self - .upper_mark - .saturating_add(self.content_rows as usize * 2) - .ge(&self.line_count) - { - self.upper_mark = self.line_count - self.content_rows as usize; + if self.upper_mark.saturating_add(self.content_rows * 2) >= self.line_count { + self.upper_mark = self.line_count - self.content_rows; return; } - self.upper_mark = self.upper_mark.saturating_add(self.content_rows.into()); + self.upper_mark = self.upper_mark.saturating_add(self.content_rows); } fn page_up(&mut self) { - let content_row_usize: usize = self.content_rows.into(); self.upper_mark = self .upper_mark - .saturating_sub(content_row_usize.saturating_add(self.line_squeezed)); + .saturating_sub(self.content_rows.saturating_add(self.line_squeezed)); if self.squeeze { let iter = self.lines.iter().take(self.upper_mark).rev(); @@ -520,7 +512,7 @@ impl<'a> Pager<'a> { // TODO: Deal with column size changes. fn page_resize(&mut self, _: u16, row: u16, option_line: Option) { if option_line.is_none() { - self.content_rows = row.saturating_sub(1); + self.content_rows = row.saturating_sub(1) as usize; }; } @@ -528,7 +520,7 @@ impl<'a> Pager<'a> { self.draw_lines(stdout); let lower_mark = self .line_count - .min(self.upper_mark.saturating_add(self.content_rows.into())); + .min(self.upper_mark.saturating_add(self.content_rows)); self.draw_prompt(stdout, lower_mark, wrong_key); stdout.flush().unwrap(); } @@ -541,7 +533,7 @@ impl<'a> Pager<'a> { let mut displayed_lines = Vec::new(); let mut iter = self.lines.iter().skip(self.upper_mark); - while displayed_lines.len() < self.content_rows as usize { + while displayed_lines.len() < self.content_rows { match iter.next() { Some(line) => { if self.squeeze { @@ -608,13 +600,12 @@ impl<'a> Pager<'a> { } } -fn search_pattern_in_file(lines: &[String], pattern: &Option) -> Option { - let pattern = pattern.clone().unwrap_or_default(); +fn search_pattern_in_file(lines: &[&str], pattern: &str) -> Option { if lines.is_empty() || pattern.is_empty() { return None; } for (line_number, line) in lines.iter().enumerate() { - if line.contains(pattern.as_str()) { + if line.contains(pattern) { return Some(line_number); } } @@ -630,8 +621,10 @@ fn paging_add_back_message(options: &Options, stdout: &mut std::io::Stdout) -> U } // Break the lines on the cols of the terminal -fn break_buff(buff: &str, cols: usize) -> Vec { - let mut lines = Vec::with_capacity(buff.lines().count()); +fn break_buff(buff: &str, cols: usize) -> Vec<&str> { + // We _could_ do a precise with_capacity here, but that would require scanning the + // whole buffer. Just guess a value instead. + let mut lines = Vec::with_capacity(2048); for l in buff.lines() { lines.append(&mut break_line(l, cols)); @@ -639,11 +632,11 @@ fn break_buff(buff: &str, cols: usize) -> Vec { lines } -fn break_line(line: &str, cols: usize) -> Vec { +fn break_line(line: &str, cols: usize) -> Vec<&str> { let width = UnicodeWidthStr::width(line); let mut lines = Vec::new(); if width < cols { - lines.push(line.to_string()); + lines.push(line); return lines; } @@ -655,14 +648,14 @@ fn break_line(line: &str, cols: usize) -> Vec { total_width += width; if total_width > cols { - lines.push(line[last_index..index].to_string()); + lines.push(&line[last_index..index]); last_index = index; total_width = width; } } if last_index != line.len() { - lines.push(line[last_index..].to_string()); + lines.push(&line[last_index..]); } lines } @@ -707,63 +700,46 @@ mod tests { test_string.push_str("👩🏻‍🔬"); } - let lines = break_line(&test_string, 80); + let lines = break_line(&test_string, 31); let widths: Vec = lines .iter() .map(|s| UnicodeWidthStr::width(&s[..])) .collect(); - // Each 👩🏻‍🔬 is 6 character width it break line to the closest number to 80 => 6 * 13 = 78 - assert_eq!((78, 42), (widths[0], widths[1])); + // Each 👩🏻‍🔬 is 2 character width, break line to the closest even number to 31 + assert_eq!((30, 10), (widths[0], widths[1])); } #[test] fn test_search_pattern_empty_lines() { let lines = vec![]; - let pattern = Some(String::from("pattern")); - assert_eq!(None, search_pattern_in_file(&lines, &pattern)); + let pattern = "pattern"; + assert_eq!(None, search_pattern_in_file(&lines, pattern)); } #[test] fn test_search_pattern_empty_pattern() { - let lines = vec![String::from("line1"), String::from("line2")]; - let pattern = None; - assert_eq!(None, search_pattern_in_file(&lines, &pattern)); + let lines = vec!["line1", "line2"]; + let pattern = ""; + assert_eq!(None, search_pattern_in_file(&lines, pattern)); } #[test] fn test_search_pattern_found_pattern() { - let lines = vec![ - String::from("line1"), - String::from("line2"), - String::from("pattern"), - ]; - let lines2 = vec![ - String::from("line1"), - String::from("line2"), - String::from("pattern"), - String::from("pattern2"), - ]; - let lines3 = vec![ - String::from("line1"), - String::from("line2"), - String::from("other_pattern"), - ]; - let pattern = Some(String::from("pattern")); - assert_eq!(2, search_pattern_in_file(&lines, &pattern).unwrap()); - assert_eq!(2, search_pattern_in_file(&lines2, &pattern).unwrap()); - assert_eq!(2, search_pattern_in_file(&lines3, &pattern).unwrap()); + let lines = vec!["line1", "line2", "pattern"]; + let lines2 = vec!["line1", "line2", "pattern", "pattern2"]; + let lines3 = vec!["line1", "line2", "other_pattern"]; + let pattern = "pattern"; + assert_eq!(2, search_pattern_in_file(&lines, pattern).unwrap()); + assert_eq!(2, search_pattern_in_file(&lines2, pattern).unwrap()); + assert_eq!(2, search_pattern_in_file(&lines3, pattern).unwrap()); } #[test] fn test_search_pattern_not_found_pattern() { - let lines = vec![ - String::from("line1"), - String::from("line2"), - String::from("something"), - ]; - let pattern = Some(String::from("pattern")); - assert_eq!(None, search_pattern_in_file(&lines, &pattern)); + let lines = vec!["line1", "line2", "something"]; + let pattern = "pattern"; + assert_eq!(None, search_pattern_in_file(&lines, pattern)); } } diff --git a/src/uu/mv/src/error.rs b/src/uu/mv/src/error.rs index f989d4e13..6daa8188e 100644 --- a/src/uu/mv/src/error.rs +++ b/src/uu/mv/src/error.rs @@ -12,7 +12,6 @@ pub enum MvError { NoSuchFile(String), CannotStatNotADirectory(String), SameFile(String, String), - SelfSubdirectory(String), SelfTargetSubdirectory(String, String), DirectoryToNonDirectory(String), NonDirectoryToDirectory(String, String), @@ -29,14 +28,9 @@ impl Display for MvError { Self::NoSuchFile(s) => write!(f, "cannot stat {s}: No such file or directory"), Self::CannotStatNotADirectory(s) => write!(f, "cannot stat {s}: Not a directory"), Self::SameFile(s, t) => write!(f, "{s} and {t} are the same file"), - Self::SelfSubdirectory(s) => write!( - f, - "cannot move '{s}' to a subdirectory of itself, '{s}/{s}'" - ), - Self::SelfTargetSubdirectory(s, t) => write!( - f, - "cannot move '{s}' to a subdirectory of itself, '{t}/{s}'" - ), + Self::SelfTargetSubdirectory(s, t) => { + write!(f, "cannot move {s} to a subdirectory of itself, {t}") + } Self::DirectoryToNonDirectory(t) => { write!(f, "cannot overwrite directory {t} with non-directory") } diff --git a/src/uu/mv/src/mv.rs b/src/uu/mv/src/mv.rs index c57f2527e..675982bac 100644 --- a/src/uu/mv/src/mv.rs +++ b/src/uu/mv/src/mv.rs @@ -19,13 +19,13 @@ use std::io; use std::os::unix; #[cfg(windows)] use std::os::windows; -use std::path::{Path, PathBuf}; +use std::path::{absolute, Path, PathBuf}; use uucore::backup_control::{self, source_is_target_backup}; use uucore::display::Quotable; use uucore::error::{set_exit_code, FromIo, UResult, USimpleError, UUsageError}; use uucore::fs::{ - are_hardlinks_or_one_way_symlink_to_same_file, are_hardlinks_to_same_file, - path_ends_with_terminator, + are_hardlinks_or_one_way_symlink_to_same_file, are_hardlinks_to_same_file, canonicalize, + path_ends_with_terminator, MissingHandling, ResolveMode, }; #[cfg(all(unix, not(any(target_os = "macos", target_os = "redox"))))] use uucore::fsxattr; @@ -322,20 +322,6 @@ fn handle_two_paths(source: &Path, target: &Path, opts: &Options) -> UResult<()> }); } - if (source.eq(target) - || are_hardlinks_to_same_file(source, target) - || are_hardlinks_or_one_way_symlink_to_same_file(source, target)) - && opts.backup == BackupMode::NoBackup - { - if source.eq(Path::new(".")) || source.ends_with("/.") || source.is_file() { - return Err( - MvError::SameFile(source.quote().to_string(), target.quote().to_string()).into(), - ); - } else { - return Err(MvError::SelfSubdirectory(source.display().to_string()).into()); - } - } - let target_is_dir = target.is_dir(); let source_is_dir = source.is_dir(); @@ -347,6 +333,8 @@ fn handle_two_paths(source: &Path, target: &Path, opts: &Options) -> UResult<()> return Err(MvError::FailedToAccessNotADirectory(target.quote().to_string()).into()); } + assert_not_same_file(source, target, target_is_dir, opts)?; + if target_is_dir { if opts.no_target_dir { if source.is_dir() { @@ -356,14 +344,6 @@ fn handle_two_paths(source: &Path, target: &Path, opts: &Options) -> UResult<()> } else { Err(MvError::DirectoryToNonDirectory(target.quote().to_string()).into()) } - // Check that source & target do not contain same subdir/dir when both exist - // mkdir dir1/dir2; mv dir1 dir1/dir2 - } else if target.starts_with(source) { - Err(MvError::SelfTargetSubdirectory( - source.display().to_string(), - target.display().to_string(), - ) - .into()) } else { move_files_into_dir(&[source.to_path_buf()], target, opts) } @@ -387,6 +367,88 @@ fn handle_two_paths(source: &Path, target: &Path, opts: &Options) -> UResult<()> } } +fn assert_not_same_file( + source: &Path, + target: &Path, + target_is_dir: bool, + opts: &Options, +) -> UResult<()> { + // we'll compare canonicalized_source and canonicalized_target for same file detection + let canonicalized_source = match canonicalize( + absolute(source)?, + MissingHandling::Normal, + ResolveMode::Logical, + ) { + Ok(source) if source.exists() => source, + _ => absolute(source)?, // file or symlink target doesn't exist but its absolute path is still used for comparison + }; + + // special case if the target exists, is a directory, and the `-T` flag wasn't used + let target_is_dir = target_is_dir && !opts.no_target_dir; + let canonicalized_target = if target_is_dir { + // `mv source_file target_dir` => target_dir/source_file + // canonicalize the path that exists (target directory) and join the source file name + canonicalize( + absolute(target)?, + MissingHandling::Normal, + ResolveMode::Logical, + )? + .join(source.file_name().unwrap_or_default()) + } else { + // `mv source target_dir/target` => target_dir/target + // we canonicalize target_dir and join /target + match absolute(target)?.parent() { + Some(parent) if parent.to_str() != Some("") => { + canonicalize(parent, MissingHandling::Normal, ResolveMode::Logical)? + .join(target.file_name().unwrap_or_default()) + } + // path.parent() returns Some("") or None if there's no parent + _ => absolute(target)?, // absolute paths should always have a parent, but we'll fall back just in case + } + }; + + let same_file = (canonicalized_source.eq(&canonicalized_target) + || are_hardlinks_to_same_file(source, target) + || are_hardlinks_or_one_way_symlink_to_same_file(source, target)) + && opts.backup == BackupMode::NoBackup; + + // get the expected target path to show in errors + // this is based on the argument and not canonicalized + let target_display = match source.file_name() { + Some(file_name) if target_is_dir => { + // join target_dir/source_file in a platform-independent manner + let mut path = target + .display() + .to_string() + .trim_end_matches("/") + .to_owned(); + + path.push('/'); + path.push_str(&file_name.to_string_lossy()); + + path.quote().to_string() + } + _ => target.quote().to_string(), + }; + + if same_file + && (canonicalized_source.eq(&canonicalized_target) + || source.eq(Path::new(".")) + || source.ends_with("/.") + || source.is_file()) + { + return Err(MvError::SameFile(source.quote().to_string(), target_display).into()); + } else if (same_file || canonicalized_target.starts_with(canonicalized_source)) + // don't error if we're moving a symlink of a directory into itself + && !source.is_symlink() + { + return Err( + MvError::SelfTargetSubdirectory(source.quote().to_string(), target_display).into(), + ); + } + Ok(()) +} + fn handle_multiple_paths(paths: &[PathBuf], opts: &Options) -> UResult<()> { if opts.no_target_dir { return Err(UUsageError::new( @@ -425,10 +487,6 @@ fn move_files_into_dir(files: &[PathBuf], target_dir: &Path, options: &Options) return Err(MvError::NotADirectory(target_dir.quote().to_string()).into()); } - let canonicalized_target_dir = target_dir - .canonicalize() - .unwrap_or_else(|_| target_dir.to_path_buf()); - let multi_progress = options.progress_bar.then(MultiProgress::new); let count_progress = if let Some(ref multi_progress) = multi_progress { @@ -479,24 +537,9 @@ fn move_files_into_dir(files: &[PathBuf], target_dir: &Path, options: &Options) // Check if we have mv dir1 dir2 dir2 // And generate an error if this is the case - if let Ok(canonicalized_source) = sourcepath.canonicalize() { - if canonicalized_source == canonicalized_target_dir { - // User tried to move directory to itself, warning is shown - // and process of moving files is continued. - show!(USimpleError::new( - 1, - format!( - "cannot move '{}' to a subdirectory of itself, '{}/{}'", - sourcepath.display(), - target_dir.display(), - canonicalized_target_dir.components().last().map_or_else( - || target_dir.display().to_string(), - |dir| { PathBuf::from(dir.as_os_str()).display().to_string() } - ) - ) - )); - continue; - } + if let Err(e) = assert_not_same_file(sourcepath, target_dir, true, options) { + show!(e); + continue; } match rename(sourcepath, &targetpath, options, multi_progress.as_ref()) { @@ -679,7 +722,7 @@ fn rename_with_fallback( }; #[cfg(all(unix, not(any(target_os = "macos", target_os = "redox"))))] - fsxattr::apply_xattrs(to, xattrs).unwrap(); + fsxattr::apply_xattrs(to, xattrs)?; if let Err(err) = result { return match err.kind { diff --git a/src/uu/od/src/inputdecoder.rs b/src/uu/od/src/inputdecoder.rs index 62117d546..44ad29228 100644 --- a/src/uu/od/src/inputdecoder.rs +++ b/src/uu/od/src/inputdecoder.rs @@ -33,7 +33,7 @@ where byte_order: ByteOrder, } -impl<'a, I> InputDecoder<'a, I> { +impl InputDecoder<'_, I> { /// Creates a new `InputDecoder` with an allocated buffer of `normal_length` + `peek_length` bytes. /// `byte_order` determines how to read multibyte formats from the buffer. pub fn new( @@ -55,7 +55,7 @@ impl<'a, I> InputDecoder<'a, I> { } } -impl<'a, I> InputDecoder<'a, I> +impl InputDecoder<'_, I> where I: PeekRead, { @@ -81,7 +81,7 @@ where } } -impl<'a, I> HasError for InputDecoder<'a, I> +impl HasError for InputDecoder<'_, I> where I: HasError, { @@ -103,7 +103,7 @@ pub struct MemoryDecoder<'a> { byte_order: ByteOrder, } -impl<'a> MemoryDecoder<'a> { +impl MemoryDecoder<'_> { /// Set a part of the internal buffer to zero. /// access to the whole buffer is possible, not just to the valid data. pub fn zero_out_buffer(&mut self, start: usize, end: usize) { diff --git a/src/uu/od/src/multifilereader.rs b/src/uu/od/src/multifilereader.rs index 813ef029f..34cd251ac 100644 --- a/src/uu/od/src/multifilereader.rs +++ b/src/uu/od/src/multifilereader.rs @@ -28,7 +28,7 @@ pub trait HasError { fn has_error(&self) -> bool; } -impl<'b> MultifileReader<'b> { +impl MultifileReader<'_> { pub fn new(fnames: Vec) -> MultifileReader { let mut mf = MultifileReader { ni: fnames, @@ -76,7 +76,7 @@ impl<'b> MultifileReader<'b> { } } -impl<'b> io::Read for MultifileReader<'b> { +impl io::Read for MultifileReader<'_> { // Fill buf with bytes read from the list of files // Returns Ok() // Handles io errors itself, thus always returns OK @@ -113,7 +113,7 @@ impl<'b> io::Read for MultifileReader<'b> { } } -impl<'b> HasError for MultifileReader<'b> { +impl HasError for MultifileReader<'_> { fn has_error(&self) -> bool { self.any_err } diff --git a/src/uu/paste/src/paste.rs b/src/uu/paste/src/paste.rs index 9d2619781..456639ba9 100644 --- a/src/uu/paste/src/paste.rs +++ b/src/uu/paste/src/paste.rs @@ -200,7 +200,7 @@ fn parse_delimiters(delimiters: &str) -> UResult]>> { let mut add_single_char_delimiter = |vec: &mut Vec>, ch: char| { let delimiter_encoded = ch.encode_utf8(&mut buffer); - vec.push(Box::from(delimiter_encoded.as_bytes())); + vec.push(Box::<[u8]>::from(delimiter_encoded.as_bytes())); }; let mut vec = Vec::>::with_capacity(delimiters.len()); @@ -311,7 +311,7 @@ impl<'a> DelimiterState<'a> { DelimiterState::MultipleDelimiters { current_delimiter, .. } => current_delimiter.len(), - _ => { + DelimiterState::NoDelimiters => { return; } }; @@ -350,7 +350,7 @@ impl<'a> DelimiterState<'a> { *current_delimiter = bo; } - _ => {} + DelimiterState::NoDelimiters => {} } } } @@ -363,8 +363,8 @@ enum InputSource { impl InputSource { fn read_until(&mut self, byte: u8, buf: &mut Vec) -> UResult { let us = match self { - Self::File(bu) => bu.read_until(byte, buf)?, - Self::StandardInput(rc) => rc + InputSource::File(bu) => bu.read_until(byte, buf)?, + InputSource::StandardInput(rc) => rc .try_borrow() .map_err(|bo| USimpleError::new(1, format!("{bo}")))? .lock() diff --git a/src/uu/rm/src/rm.rs b/src/uu/rm/src/rm.rs index a89ba6db6..ad9c942a8 100644 --- a/src/uu/rm/src/rm.rs +++ b/src/uu/rm/src/rm.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 (path) eacces inacc +// spell-checker:ignore (path) eacces inacc rm-r4 use clap::{builder::ValueParser, crate_version, parser::ValueSource, Arg, ArgAction, Command}; use std::collections::VecDeque; @@ -11,10 +11,15 @@ use std::ffi::{OsStr, OsString}; use std::fs::{self, File, Metadata}; use std::io::ErrorKind; use std::ops::BitOr; +#[cfg(not(windows))] +use std::os::unix::ffi::OsStrExt; +use std::path::MAIN_SEPARATOR; use std::path::{Path, PathBuf}; use uucore::display::Quotable; use uucore::error::{UResult, USimpleError, UUsageError}; -use uucore::{format_usage, help_about, help_section, help_usage, prompt_yes, show_error}; +use uucore::{ + format_usage, help_about, help_section, help_usage, os_str_as_bytes, prompt_yes, show_error, +}; use walkdir::{DirEntry, WalkDir}; #[derive(Eq, PartialEq, Clone, Copy)] @@ -290,6 +295,7 @@ pub fn remove(files: &[&OsStr], options: &Options) -> bool { for filename in files { let file = Path::new(filename); + had_err = match file.symlink_metadata() { Ok(metadata) => { if metadata.is_dir() { @@ -300,6 +306,7 @@ pub fn remove(files: &[&OsStr], options: &Options) -> bool { remove_file(file, options) } } + Err(_e) => { // TODO: actually print out the specific error // TODO: When the error is not about missing files @@ -326,6 +333,15 @@ pub fn remove(files: &[&OsStr], options: &Options) -> bool { fn handle_dir(path: &Path, options: &Options) -> bool { let mut had_err = false; + let path = clean_trailing_slashes(path); + if path_is_current_or_parent_directory(path) { + show_error!( + "refusing to remove '.' or '..' directory: skipping '{}'", + path.display() + ); + return true; + } + let is_root = path.has_root() && path.parent().is_none(); if options.recursive && (!is_root || !options.preserve_root) { if options.interactive != InteractiveMode::Always && !options.verbose { @@ -396,7 +412,11 @@ fn handle_dir(path: &Path, options: &Options) -> bool { } else if options.dir && (!is_root || !options.preserve_root) { had_err = remove_dir(path, options).bitor(had_err); } else if options.recursive { - show_error!("could not remove directory {}", path.quote()); + show_error!( + "it is dangerous to operate recursively on '{}'", + MAIN_SEPARATOR + ); + show_error!("use --no-preserve-root to override this failsafe"); had_err = true; } else { show_error!( @@ -559,6 +579,20 @@ fn handle_writable_directory(path: &Path, options: &Options, metadata: &Metadata true } } +/// Checks if the path is referring to current or parent directory , if it is referring to current or any parent directory in the file tree e.g '/../..' , '../..' +fn path_is_current_or_parent_directory(path: &Path) -> bool { + let path_str = os_str_as_bytes(path.as_os_str()); + let dir_separator = MAIN_SEPARATOR as u8; + if let Ok(path_bytes) = path_str { + return path_bytes == ([b'.']) + || path_bytes == ([b'.', b'.']) + || path_bytes.ends_with(&[dir_separator, b'.']) + || path_bytes.ends_with(&[dir_separator, b'.', b'.']) + || path_bytes.ends_with(&[dir_separator, b'.', dir_separator]) + || path_bytes.ends_with(&[dir_separator, b'.', b'.', dir_separator]); + } + false +} // For windows we can use windows metadata trait and file attributes to see if a directory is readonly #[cfg(windows)] @@ -586,6 +620,40 @@ fn handle_writable_directory(path: &Path, options: &Options, metadata: &Metadata } } +/// Removes trailing slashes, for example 'd/../////' yield 'd/../' required to fix rm-r4 GNU test +fn clean_trailing_slashes(path: &Path) -> &Path { + let path_str = os_str_as_bytes(path.as_os_str()); + let dir_separator = MAIN_SEPARATOR as u8; + + if let Ok(path_bytes) = path_str { + let mut idx = if path_bytes.len() > 1 { + path_bytes.len() - 1 + } else { + return path; + }; + // Checks if element at the end is a '/' + if path_bytes[idx] == dir_separator { + for i in (1..path_bytes.len()).rev() { + // Will break at the start of the continuous sequence of '/', eg: "abc//////" , will break at + // "abc/", this will clean ////// to the root '/', so we have to be careful to not + // delete the root. + if path_bytes[i - 1] != dir_separator { + idx = i; + break; + } + } + #[cfg(unix)] + return Path::new(OsStr::from_bytes(&path_bytes[0..=idx])); + + #[cfg(not(unix))] + // Unwrapping is fine here as os_str_as_bytes() would return an error on non unix + // systems with non utf-8 characters and thus bypass the if let Ok branch + return Path::new(std::str::from_utf8(&path_bytes[0..=idx]).unwrap()); + } + } + path +} + fn prompt_descend(path: &Path) -> bool { prompt_yes!("descend into directory {}?", path.quote()) } @@ -611,3 +679,17 @@ fn is_symlink_dir(metadata: &Metadata) -> bool { metadata.file_type().is_symlink() && ((metadata.file_attributes() & FILE_ATTRIBUTE_DIRECTORY) != 0) } + +mod tests { + + #[test] + // Testing whether path the `/////` collapses to `/` + fn test_collapsible_slash_path() { + use std::path::Path; + + use crate::clean_trailing_slashes; + let path = Path::new("/////"); + + assert_eq!(Path::new("/"), clean_trailing_slashes(path)); + } +} diff --git a/src/uu/seq/src/numberparse.rs b/src/uu/seq/src/numberparse.rs index 5a5c64bb9..c8dec0180 100644 --- a/src/uu/seq/src/numberparse.rs +++ b/src/uu/seq/src/numberparse.rs @@ -102,20 +102,33 @@ fn parse_exponent_no_decimal(s: &str, j: usize) -> Result() + .map_err(|_| ParseNumberError::Float)?; + if parsed_decimal == BigDecimal::zero() { + BigDecimal::zero() + } else { + parsed_decimal + } + }; let num_integral_digits = if is_minus_zero_float(s, &x) { if exponent > 0 { - 2usize + exponent as usize + (2usize) + .checked_add(exponent as usize) + .ok_or(ParseNumberError::Float)? } else { 2usize } } else { - let total = j as i64 + exponent; + let total = (j as i64) + .checked_add(exponent) + .ok_or(ParseNumberError::Float)?; let result = if total < 1 { 1 } else { - total.try_into().unwrap() + total.try_into().map_err(|_| ParseNumberError::Float)? }; if x.sign() == Sign::Minus { result + 1 @@ -200,14 +213,25 @@ fn parse_decimal_and_exponent( // Because of the match guard, this subtraction will not underflow. let num_digits_between_decimal_point_and_e = (j - (i + 1)) as i64; let exponent: i64 = s[j + 1..].parse().map_err(|_| ParseNumberError::Float)?; - let val: BigDecimal = s.parse().map_err(|_| ParseNumberError::Float)?; + let val: BigDecimal = { + let parsed_decimal = s + .parse::() + .map_err(|_| ParseNumberError::Float)?; + if parsed_decimal == BigDecimal::zero() { + BigDecimal::zero() + } else { + parsed_decimal + } + }; let num_integral_digits = { let minimum: usize = { let integral_part: f64 = s[..j].parse().map_err(|_| ParseNumberError::Float)?; if integral_part.is_sign_negative() { if exponent > 0 { - 2usize + exponent as usize + 2usize + .checked_add(exponent as usize) + .ok_or(ParseNumberError::Float)? } else { 2usize } @@ -217,15 +241,20 @@ fn parse_decimal_and_exponent( }; // Special case: if the string is "-.1e2", we need to treat it // as if it were "-0.1e2". - let total = if s.starts_with("-.") { - i as i64 + exponent + 1 - } else { - i as i64 + exponent + let total = { + let total = (i as i64) + .checked_add(exponent) + .ok_or(ParseNumberError::Float)?; + if s.starts_with("-.") { + total.checked_add(1).ok_or(ParseNumberError::Float)? + } else { + total + } }; if total < minimum as i64 { minimum } else { - total.try_into().unwrap() + total.try_into().map_err(|_| ParseNumberError::Float)? } }; @@ -312,7 +341,7 @@ impl FromStr for PreciseNumber { // Check if the string seems to be in hexadecimal format. // // May be 0x123 or -0x123, so the index `i` may be either 0 or 1. - if let Some(i) = s.to_lowercase().find("0x") { + if let Some(i) = s.find("0x").or_else(|| s.find("0X")) { if i <= 1 { return parse_hexadecimal(s); } @@ -322,7 +351,7 @@ impl FromStr for PreciseNumber { // number differently depending on its form. This is important // because the form of the input dictates how the output will be // presented. - match (s.find('.'), s.find('e')) { + match (s.find('.'), s.find(['e', 'E'])) { // For example, "123456" or "inf". (None, None) => parse_no_decimal_no_exponent(s), // For example, "123e456" or "1e-2". @@ -381,6 +410,7 @@ mod tests { fn test_parse_big_int() { assert_eq!(parse("0"), ExtendedBigDecimal::zero()); assert_eq!(parse("0.1e1"), ExtendedBigDecimal::one()); + assert_eq!(parse("0.1E1"), ExtendedBigDecimal::one()); assert_eq!( parse("1.0e1"), ExtendedBigDecimal::BigDecimal("10".parse::().unwrap()) diff --git a/src/uu/seq/src/seq.rs b/src/uu/seq/src/seq.rs index 96ae83ba0..e14ba35a9 100644 --- a/src/uu/seq/src/seq.rs +++ b/src/uu/seq/src/seq.rs @@ -3,6 +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) extendedbigdecimal numberparse +use std::ffi::OsString; use std::io::{stdout, ErrorKind, Write}; use clap::{crate_version, Arg, ArgAction, Command}; @@ -47,9 +48,33 @@ struct SeqOptions<'a> { /// The elements are (first, increment, last). type RangeFloat = (ExtendedBigDecimal, ExtendedBigDecimal, ExtendedBigDecimal); +// Turn short args with attached value, for example "-s,", into two args "-s" and "," to make +// them work with clap. +fn split_short_args_with_value(args: impl uucore::Args) -> impl uucore::Args { + let mut v: Vec = Vec::new(); + + for arg in args { + let bytes = arg.as_encoded_bytes(); + + if bytes.len() > 2 + && (bytes.starts_with(b"-f") || bytes.starts_with(b"-s") || bytes.starts_with(b"-t")) + { + let (short_arg, value) = bytes.split_at(2); + // SAFETY: + // Both `short_arg` and `value` only contain content that originated from `OsStr::as_encoded_bytes` + v.push(unsafe { OsString::from_encoded_bytes_unchecked(short_arg.to_vec()) }); + v.push(unsafe { OsString::from_encoded_bytes_unchecked(value.to_vec()) }); + } else { + v.push(arg); + } + } + + v.into_iter() +} + #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().try_get_matches_from(split_short_args_with_value(args))?; let numbers_option = matches.get_many::(ARG_NUMBERS); @@ -138,7 +163,6 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .trailing_var_arg(true) - .allow_negative_numbers(true) .infer_long_args(true) .version(crate_version!()) .about(ABOUT) @@ -169,7 +193,10 @@ pub fn uu_app() -> Command { .help("use printf style floating-point FORMAT"), ) .arg( + // we use allow_hyphen_values instead of allow_negative_numbers because clap removed + // the support for "exotic" negative numbers like -.1 (see https://github.com/clap-rs/clap/discussions/5837) Arg::new(ARG_NUMBERS) + .allow_hyphen_values(true) .action(ArgAction::Append) .num_args(1..=3), ) diff --git a/src/uu/shuf/src/shuf.rs b/src/uu/shuf/src/shuf.rs index 260b5130c..2d8023448 100644 --- a/src/uu/shuf/src/shuf.rs +++ b/src/uu/shuf/src/shuf.rs @@ -279,7 +279,10 @@ impl<'a> Shufable for Vec<&'a [u8]> { // this is safe. (**self).choose(rng).unwrap() } - type PartialShuffleIterator<'b> = std::iter::Copied> where Self: 'b; + type PartialShuffleIterator<'b> + = std::iter::Copied> + where + Self: 'b; fn partial_shuffle<'b>( &'b mut self, rng: &'b mut WrappedRng, @@ -298,7 +301,10 @@ impl Shufable for RangeInclusive { fn choose(&self, rng: &mut WrappedRng) -> usize { rng.gen_range(self.clone()) } - type PartialShuffleIterator<'b> = NonrepeatingIterator<'b> where Self: 'b; + type PartialShuffleIterator<'b> + = NonrepeatingIterator<'b> + where + Self: 'b; fn partial_shuffle<'b>( &'b mut self, rng: &'b mut WrappedRng, @@ -374,7 +380,7 @@ impl<'a> NonrepeatingIterator<'a> { } } -impl<'a> Iterator for NonrepeatingIterator<'a> { +impl Iterator for NonrepeatingIterator<'_> { type Item = usize; fn next(&mut self) -> Option { @@ -401,7 +407,7 @@ trait Writable { fn write_all_to(&self, output: &mut impl Write) -> Result<(), Error>; } -impl<'a> Writable for &'a [u8] { +impl Writable for &[u8] { fn write_all_to(&self, output: &mut impl Write) -> Result<(), Error> { output.write_all(self) } diff --git a/src/uu/sort/src/ext_sort.rs b/src/uu/sort/src/ext_sort.rs index 183098812..57e434e99 100644 --- a/src/uu/sort/src/ext_sort.rs +++ b/src/uu/sort/src/ext_sort.rs @@ -98,12 +98,12 @@ fn reader_writer< )?; match read_result { ReadResult::WroteChunksToFile { tmp_files } => { - let merger = merge::merge_with_file_limit::<_, _, Tmp>( + merge::merge_with_file_limit::<_, _, Tmp>( tmp_files.into_iter().map(|c| c.reopen()), settings, + output, tmp_dir, )?; - merger.write_all(settings, output)?; } ReadResult::SortedSingleChunk(chunk) => { if settings.unique { diff --git a/src/uu/sort/src/merge.rs b/src/uu/sort/src/merge.rs index c0457ffa4..300733d1e 100644 --- a/src/uu/sort/src/merge.rs +++ b/src/uu/sort/src/merge.rs @@ -25,7 +25,6 @@ use std::{ }; use compare::Compare; -use itertools::Itertools; use uucore::error::UResult; use crate::{ @@ -67,58 +66,63 @@ fn replace_output_file_in_input_files( /// /// If `settings.merge_batch_size` is greater than the length of `files`, intermediate files will be used. /// If `settings.compress_prog` is `Some`, intermediate files will be compressed with it. -pub fn merge<'a>( +pub fn merge( files: &mut [OsString], - settings: &'a GlobalSettings, - output: Option<&str>, + settings: &GlobalSettings, + output: Output, tmp_dir: &mut TmpDirWrapper, -) -> UResult> { - replace_output_file_in_input_files(files, output, tmp_dir)?; +) -> UResult<()> { + replace_output_file_in_input_files(files, output.as_output_name(), tmp_dir)?; + let files = files + .iter() + .map(|file| open(file).map(|file| PlainMergeInput { inner: file })); if settings.compress_prog.is_none() { - merge_with_file_limit::<_, _, WriteablePlainTmpFile>( - files - .iter() - .map(|file| open(file).map(|file| PlainMergeInput { inner: file })), - settings, - tmp_dir, - ) + merge_with_file_limit::<_, _, WriteablePlainTmpFile>(files, settings, output, tmp_dir) } else { - merge_with_file_limit::<_, _, WriteableCompressedTmpFile>( - files - .iter() - .map(|file| open(file).map(|file| PlainMergeInput { inner: file })), - settings, - tmp_dir, - ) + merge_with_file_limit::<_, _, WriteableCompressedTmpFile>(files, settings, output, tmp_dir) } } // Merge already sorted `MergeInput`s. pub fn merge_with_file_limit< - 'a, M: MergeInput + 'static, F: ExactSizeIterator>, Tmp: WriteableTmpFile + 'static, >( files: F, - settings: &'a GlobalSettings, + settings: &GlobalSettings, + output: Output, tmp_dir: &mut TmpDirWrapper, -) -> UResult> { - if files.len() > settings.merge_batch_size { - let mut remaining_files = files.len(); - let batches = files.chunks(settings.merge_batch_size); - let mut batches = batches.into_iter(); +) -> UResult<()> { + if files.len() <= settings.merge_batch_size { + let merger = merge_without_limit(files, settings); + merger?.write_all(settings, output) + } else { let mut temporary_files = vec![]; - while remaining_files != 0 { - // Work around the fact that `Chunks` is not an `ExactSizeIterator`. - remaining_files = remaining_files.saturating_sub(settings.merge_batch_size); - let merger = merge_without_limit(batches.next().unwrap(), settings)?; + let mut batch = vec![]; + for file in files { + batch.push(file); + if batch.len() >= settings.merge_batch_size { + assert_eq!(batch.len(), settings.merge_batch_size); + let merger = merge_without_limit(batch.into_iter(), settings)?; + batch = vec![]; + + let mut tmp_file = + Tmp::create(tmp_dir.next_file()?, settings.compress_prog.as_deref())?; + merger.write_all_to(settings, tmp_file.as_write())?; + temporary_files.push(tmp_file.finished_writing()?); + } + } + // Merge any remaining files that didn't get merged in a full batch above. + if !batch.is_empty() { + assert!(batch.len() < settings.merge_batch_size); + let merger = merge_without_limit(batch.into_iter(), settings)?; + let mut tmp_file = Tmp::create(tmp_dir.next_file()?, settings.compress_prog.as_deref())?; merger.write_all_to(settings, tmp_file.as_write())?; temporary_files.push(tmp_file.finished_writing()?); } - assert!(batches.next().is_none()); merge_with_file_limit::<_, _, Tmp>( temporary_files .into_iter() @@ -127,10 +131,9 @@ pub fn merge_with_file_limit< dyn FnMut(Tmp::Closed) -> UResult<::Reopened>, >), settings, + output, tmp_dir, ) - } else { - merge_without_limit(files, settings) } } @@ -260,21 +263,21 @@ struct PreviousLine { } /// Merges files together. This is **not** an iterator because of lifetime problems. -pub struct FileMerger<'a> { +struct FileMerger<'a> { heap: binary_heap_plus::BinaryHeap>, request_sender: Sender<(usize, RecycledChunk)>, prev: Option, reader_join_handle: JoinHandle>, } -impl<'a> FileMerger<'a> { +impl FileMerger<'_> { /// Write the merged contents to the output file. - pub fn write_all(self, settings: &GlobalSettings, output: Output) -> UResult<()> { + fn write_all(self, settings: &GlobalSettings, output: Output) -> UResult<()> { let mut out = output.into_write(); self.write_all_to(settings, &mut out) } - pub fn write_all_to(mut self, settings: &GlobalSettings, out: &mut impl Write) -> UResult<()> { + fn write_all_to(mut self, settings: &GlobalSettings, out: &mut impl Write) -> UResult<()> { while self.write_next(settings, out) {} drop(self.request_sender); self.reader_join_handle.join().unwrap() @@ -341,7 +344,7 @@ struct FileComparator<'a> { settings: &'a GlobalSettings, } -impl<'a> Compare for FileComparator<'a> { +impl Compare for FileComparator<'_> { fn compare(&self, a: &MergeableFile, b: &MergeableFile) -> Ordering { let mut cmp = compare_by( &a.current_chunk.lines()[a.line_idx], diff --git a/src/uu/sort/src/sort.rs b/src/uu/sort/src/sort.rs index c2e752bdf..8b6fcbb25 100644 --- a/src/uu/sort/src/sort.rs +++ b/src/uu/sort/src/sort.rs @@ -1567,8 +1567,7 @@ fn exec( tmp_dir: &mut TmpDirWrapper, ) -> UResult<()> { if settings.merge { - let file_merger = merge::merge(files, settings, output.as_output_name(), tmp_dir)?; - file_merger.write_all(settings, output) + merge::merge(files, settings, output, tmp_dir) } else if settings.check { if files.len() > 1 { Err(UUsageError::new(2, "only one file allowed with -c")) diff --git a/src/uu/split/src/filenames.rs b/src/uu/split/src/filenames.rs index d2ce1beb3..9e899a417 100644 --- a/src/uu/split/src/filenames.rs +++ b/src/uu/split/src/filenames.rs @@ -341,7 +341,7 @@ impl<'a> FilenameIterator<'a> { } } -impl<'a> Iterator for FilenameIterator<'a> { +impl Iterator for FilenameIterator<'_> { type Item = String; fn next(&mut self) -> Option { diff --git a/src/uu/split/src/split.rs b/src/uu/split/src/split.rs index 11fa04184..279e91dae 100644 --- a/src/uu/split/src/split.rs +++ b/src/uu/split/src/split.rs @@ -492,7 +492,7 @@ impl Settings { } match first.as_str() { "\\0" => b'\0', - s if s.as_bytes().len() == 1 => s.as_bytes()[0], + s if s.len() == 1 => s.as_bytes()[0], s => return Err(SettingsError::MultiCharacterSeparator(s.to_string())), } } @@ -748,7 +748,7 @@ impl<'a> ByteChunkWriter<'a> { } } -impl<'a> Write for ByteChunkWriter<'a> { +impl Write for ByteChunkWriter<'_> { /// Implements `--bytes=SIZE` fn write(&mut self, mut buf: &[u8]) -> std::io::Result { // If the length of `buf` exceeds the number of bytes remaining @@ -872,7 +872,7 @@ impl<'a> LineChunkWriter<'a> { } } -impl<'a> Write for LineChunkWriter<'a> { +impl Write for LineChunkWriter<'_> { /// Implements `--lines=NUMBER` fn write(&mut self, buf: &[u8]) -> std::io::Result { // If the number of lines in `buf` exceeds the number of lines @@ -978,7 +978,7 @@ impl<'a> LineBytesChunkWriter<'a> { } } -impl<'a> Write for LineBytesChunkWriter<'a> { +impl Write for LineBytesChunkWriter<'_> { /// Write as many lines to a chunk as possible without /// exceeding the byte limit. If a single line has more bytes /// than the limit, then fill an entire single chunk with those diff --git a/src/uu/stat/src/stat.rs b/src/uu/stat/src/stat.rs index ee4178344..5e617e7a3 100644 --- a/src/uu/stat/src/stat.rs +++ b/src/uu/stat/src/stat.rs @@ -9,7 +9,9 @@ use uucore::error::{UResult, USimpleError}; use clap::builder::ValueParser; use uucore::display::Quotable; use uucore::fs::display_permissions; -use uucore::fsext::{pretty_filetype, pretty_fstype, read_fs_list, statfs, BirthTime, FsMeta}; +use uucore::fsext::{ + pretty_filetype, pretty_fstype, read_fs_list, statfs, BirthTime, FsMeta, StatFs, +}; use uucore::libc::mode_t; use uucore::{ entries, format_usage, help_about, help_section, help_usage, show_error, show_warning, @@ -19,10 +21,12 @@ use chrono::{DateTime, Local}; use clap::{crate_version, Arg, ArgAction, ArgMatches, Command}; use std::borrow::Cow; use std::ffi::{OsStr, OsString}; -use std::fs; +use std::fs::{FileType, Metadata}; +use std::io::Write; use std::os::unix::fs::{FileTypeExt, MetadataExt}; use std::os::unix::prelude::OsStrExt; use std::path::Path; +use std::{env, fs}; const ABOUT: &str = help_about!("stat.md"); const USAGE: &str = help_usage!("stat.md"); @@ -93,9 +97,33 @@ pub enum OutputType { Unknown, } +#[derive(Default)] +enum QuotingStyle { + Locale, + Shell, + #[default] + ShellEscapeAlways, + Quote, +} + +impl std::str::FromStr for QuotingStyle { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "locale" => Ok(QuotingStyle::Locale), + "shell" => Ok(QuotingStyle::Shell), + "shell-escape-always" => Ok(QuotingStyle::ShellEscapeAlways), + // The others aren't exposed to the user + _ => Err(format!("Invalid quoting style: {}", s)), + } + } +} + #[derive(Debug, PartialEq, Eq)] enum Token { Char(char), + Byte(u8), Directive { flag: Flags, width: usize, @@ -293,6 +321,93 @@ fn print_str(s: &str, flags: &Flags, width: usize, precision: Option) { pad_and_print(s, flags.left, width, Padding::Space); } +fn quote_file_name(file_name: &str, quoting_style: &QuotingStyle) -> String { + match quoting_style { + QuotingStyle::Locale | QuotingStyle::Shell => { + let escaped = file_name.replace('\'', r"\'"); + format!("'{}'", escaped) + } + QuotingStyle::ShellEscapeAlways => format!("\"{}\"", file_name), + QuotingStyle::Quote => file_name.to_string(), + } +} + +fn get_quoted_file_name( + display_name: &str, + file: &OsString, + file_type: &FileType, + from_user: bool, +) -> Result { + let quoting_style = env::var("QUOTING_STYLE") + .ok() + .and_then(|style| style.parse().ok()) + .unwrap_or_default(); + + if file_type.is_symlink() { + let quoted_display_name = quote_file_name(display_name, "ing_style); + match fs::read_link(file) { + Ok(dst) => { + let quoted_dst = quote_file_name(&dst.to_string_lossy(), "ing_style); + Ok(format!("{quoted_display_name} -> {quoted_dst}")) + } + Err(e) => { + show_error!("{e}"); + Err(1) + } + } + } else { + let style = if from_user { + quoting_style + } else { + QuotingStyle::Quote + }; + Ok(quote_file_name(display_name, &style)) + } +} + +fn process_token_filesystem(t: &Token, meta: StatFs, display_name: &str) { + match *t { + Token::Byte(byte) => write_raw_byte(byte), + Token::Char(c) => print!("{c}"), + Token::Directive { + flag, + width, + precision, + format, + } => { + let output = match format { + // free blocks available to non-superuser + 'a' => OutputType::Unsigned(meta.avail_blocks()), + // total data blocks in file system + 'b' => OutputType::Unsigned(meta.total_blocks()), + // total file nodes in file system + 'c' => OutputType::Unsigned(meta.total_file_nodes()), + // free file nodes in file system + 'd' => OutputType::Unsigned(meta.free_file_nodes()), + // free blocks in file system + 'f' => OutputType::Unsigned(meta.free_blocks()), + // file system ID in hex + 'i' => OutputType::UnsignedHex(meta.fsid()), + // maximum length of filenames + 'l' => OutputType::Unsigned(meta.namelen()), + // file name + 'n' => OutputType::Str(display_name.to_string()), + // block size (for faster transfers) + 's' => OutputType::Unsigned(meta.io_size()), + // fundamental block size (for block counts) + 'S' => OutputType::Integer(meta.block_size()), + // file system type in hex + 't' => OutputType::UnsignedHex(meta.fs_type() as u64), + // file system type in human readable form + 'T' => OutputType::Str(pretty_fstype(meta.fs_type()).into()), + _ => OutputType::Unknown, + }; + + print_it(&output, flag, width, precision); + } + } +} + /// Prints an integer value based on the provided flags, width, and precision. /// /// # Arguments @@ -403,7 +518,26 @@ fn print_unsigned_hex( pad_and_print(&s, flags.left, width, padding_char); } +fn write_raw_byte(byte: u8) { + std::io::stdout().write_all(&[byte]).unwrap(); +} + impl Stater { + fn process_flags(chars: &[char], i: &mut usize, bound: usize, flag: &mut Flags) { + while *i < bound { + match chars[*i] { + '#' => flag.alter = true, + '0' => flag.zero = true, + '-' => flag.left = true, + ' ' => flag.space = true, + '+' => flag.sign = true, + '\'' => flag.group = true, + _ => break, + } + *i += 1; + } + } + fn handle_percent_case( chars: &[char], i: &mut usize, @@ -423,20 +557,7 @@ impl Stater { let mut flag = Flags::default(); - while *i < bound { - match chars[*i] { - '#' => flag.alter = true, - '0' => flag.zero = true, - '-' => flag.left = true, - ' ' => flag.space = true, - '+' => flag.sign = true, - '\'' => flag.group = true, - 'I' => unimplemented!(), - _ => break, - } - *i += 1; - } - check_bound(format_str, bound, old, *i)?; + Self::process_flags(chars, i, bound, &mut flag); let mut width = 0; let mut precision = None; @@ -445,6 +566,15 @@ impl Stater { if let Some((field_width, offset)) = format_str[j..].scan_num::() { width = field_width; j += offset; + + // Reject directives like `%` by checking if width has been parsed. + if j >= bound || chars[j] == '%' { + let invalid_directive: String = chars[old..=j.min(bound - 1)].iter().collect(); + return Err(USimpleError::new( + 1, + format!("{}: invalid directive", invalid_directive.quote()), + )); + } } check_bound(format_str, bound, old, j)?; @@ -465,9 +595,27 @@ impl Stater { } *i = j; + + // Check for multi-character specifiers (e.g., `%Hd`, `%Lr`) + if *i + 1 < bound { + 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!("{}{}", chars[*i], next_char); + *i += 1; + return Ok(Token::Directive { + flag, + width, + precision, + format: specifier.chars().next().unwrap(), + }); + } + } + } + Ok(Token::Directive { - width, flag, + width, precision, format: chars[*i], }) @@ -485,33 +633,49 @@ impl Stater { return Token::Char('\\'); } match chars[*i] { - 'x' if *i + 1 < bound => { - if let Some((c, offset)) = format_str[*i + 1..].scan_char(16) { - *i += offset; - Token::Char(c) + 'a' => Token::Byte(0x07), // BEL + 'b' => Token::Byte(0x08), // Backspace + 'f' => Token::Byte(0x0C), // Form feed + 'n' => Token::Byte(0x0A), // Line feed + 'r' => Token::Byte(0x0D), // Carriage return + 't' => Token::Byte(0x09), // Horizontal tab + '\\' => Token::Byte(b'\\'), // Backslash + '\'' => Token::Byte(b'\''), // Single quote + '"' => Token::Byte(b'"'), // Double quote + '0'..='7' => { + // Parse octal escape sequence (up to 3 digits) + let mut value = 0u8; + let mut count = 0; + while *i < bound && count < 3 { + if let Some(digit) = chars[*i].to_digit(8) { + value = value * 8 + digit as u8; + *i += 1; + count += 1; + } else { + break; + } + } + *i -= 1; // Adjust index to account for the outer loop increment + Token::Byte(value) + } + 'x' => { + // Parse hexadecimal escape sequence + if *i + 1 < bound { + if let Some((c, offset)) = format_str[*i + 1..].scan_char(16) { + *i += offset; + Token::Byte(c as u8) + } else { + show_warning!("unrecognized escape '\\x'"); + Token::Byte(b'x') + } } else { - show_warning!("unrecognized escape '\\x'"); - Token::Char('x') + show_warning!("incomplete hex escape '\\x'"); + Token::Byte(b'x') } } - '0'..='7' => { - let (c, offset) = format_str[*i..].scan_char(8).unwrap(); - *i += offset - 1; - Token::Char(c) - } - '"' => Token::Char('"'), - '\\' => Token::Char('\\'), - 'a' => Token::Char('\x07'), - 'b' => Token::Char('\x08'), - 'e' => Token::Char('\x1B'), - 'f' => Token::Char('\x0C'), - 'n' => Token::Char('\n'), - 'r' => Token::Char('\r'), - 't' => Token::Char('\t'), - 'v' => Token::Char('\x0B'), - c => { - show_warning!("unrecognized escape '\\{}'", c); - Token::Char(c) + other => { + show_warning!("unrecognized escape '\\{}'", other); + Token::Byte(other as u8) } } } @@ -634,7 +798,128 @@ impl Stater { ret } - #[allow(clippy::cognitive_complexity)] + fn process_token_files( + &self, + t: &Token, + meta: &Metadata, + display_name: &str, + file: &OsString, + file_type: &FileType, + from_user: bool, + ) -> Result<(), i32> { + match *t { + Token::Byte(byte) => write_raw_byte(byte), + Token::Char(c) => print!("{c}"), + + Token::Directive { + flag, + width, + precision, + format, + } => { + let output = match format { + // access rights in octal + 'a' => OutputType::UnsignedOct(0o7777 & meta.mode()), + // access rights in human readable form + 'A' => OutputType::Str(display_permissions(meta, true)), + // number of blocks allocated (see %B) + 'b' => OutputType::Unsigned(meta.blocks()), + + // the size in bytes of each block reported by %b + // FIXME: blocksize differs on various platform + // See coreutils/gnulib/lib/stat-size.h ST_NBLOCKSIZE // spell-checker:disable-line + 'B' => OutputType::Unsigned(512), + + // device number in decimal + 'd' => OutputType::Unsigned(meta.dev()), + // device number in hex + 'D' => OutputType::UnsignedHex(meta.dev()), + // raw mode in hex + 'f' => OutputType::UnsignedHex(meta.mode() as u64), + // file type + 'F' => OutputType::Str( + pretty_filetype(meta.mode() as mode_t, meta.len()).to_owned(), + ), + // group ID of owner + 'g' => OutputType::Unsigned(meta.gid() as u64), + // group name of owner + 'G' => { + let group_name = + entries::gid2grp(meta.gid()).unwrap_or_else(|_| "UNKNOWN".to_owned()); + OutputType::Str(group_name) + } + // number of hard links + 'h' => OutputType::Unsigned(meta.nlink()), + // inode number + 'i' => OutputType::Unsigned(meta.ino()), + // mount point + 'm' => OutputType::Str(self.find_mount_point(file).unwrap()), + // file name + 'n' => OutputType::Str(display_name.to_string()), + // quoted file name with dereference if symbolic link + 'N' => { + let file_name = + get_quoted_file_name(display_name, file, file_type, from_user)?; + OutputType::Str(file_name) + } + // optimal I/O transfer size hint + 'o' => OutputType::Unsigned(meta.blksize()), + // total size, in bytes + 's' => OutputType::Integer(meta.len() as i64), + // major device type in hex, for character/block device special + // files + 't' => OutputType::UnsignedHex(meta.rdev() >> 8), + // minor device type in hex, for character/block device special + // files + 'T' => OutputType::UnsignedHex(meta.rdev() & 0xff), + // user ID of owner + 'u' => OutputType::Unsigned(meta.uid() as u64), + // user name of owner + 'U' => { + let user_name = + entries::uid2usr(meta.uid()).unwrap_or_else(|_| "UNKNOWN".to_owned()); + OutputType::Str(user_name) + } + + // time of file birth, human-readable; - if unknown + 'w' => OutputType::Str( + meta.birth() + .map(|(sec, nsec)| pretty_time(sec as i64, nsec as i64)) + .unwrap_or(String::from("-")), + ), + + // time of file birth, seconds since Epoch; 0 if unknown + 'W' => OutputType::Unsigned(meta.birth().unwrap_or_default().0), + + // time of last access, human-readable + 'x' => OutputType::Str(pretty_time(meta.atime(), meta.atime_nsec())), + // time of last access, seconds since Epoch + 'X' => OutputType::Integer(meta.atime()), + // time of last data modification, human-readable + 'y' => OutputType::Str(pretty_time(meta.mtime(), meta.mtime_nsec())), + // time of last data modification, seconds since Epoch + 'Y' => OutputType::Integer(meta.mtime()), + // time of last status change, human-readable + 'z' => OutputType::Str(pretty_time(meta.ctime(), meta.ctime_nsec())), + // time of last status change, seconds since Epoch + 'Z' => OutputType::Integer(meta.ctime()), + 'R' => { + let major = meta.rdev() >> 8; + let minor = meta.rdev() & 0xff; + OutputType::Str(format!("{},{}", major, minor)) + } + '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); + } + } + Ok(()) + } + fn do_stat(&self, file: &OsStr, stdin_is_fifo: bool) -> i32 { let display_name = file.to_string_lossy(); let file = if cfg!(unix) && display_name == "-" { @@ -659,46 +944,9 @@ impl Stater { Ok(meta) => { let tokens = &self.default_tokens; + // Usage for t in tokens { - match *t { - Token::Char(c) => print!("{c}"), - Token::Directive { - flag, - width, - precision, - format, - } => { - let output = match format { - // free blocks available to non-superuser - 'a' => OutputType::Unsigned(meta.avail_blocks()), - // total data blocks in file system - 'b' => OutputType::Unsigned(meta.total_blocks()), - // total file nodes in file system - 'c' => OutputType::Unsigned(meta.total_file_nodes()), - // free file nodes in file system - 'd' => OutputType::Unsigned(meta.free_file_nodes()), - // free blocks in file system - 'f' => OutputType::Unsigned(meta.free_blocks()), - // file system ID in hex - 'i' => OutputType::UnsignedHex(meta.fsid()), - // maximum length of filenames - 'l' => OutputType::Unsigned(meta.namelen()), - // file name - 'n' => OutputType::Str(display_name.to_string()), - // block size (for faster transfers) - 's' => OutputType::Unsigned(meta.io_size()), - // fundamental block size (for block counts) - 'S' => OutputType::Integer(meta.block_size()), - // file system type in hex - 't' => OutputType::UnsignedHex(meta.fs_type() as u64), - // file system type in human readable form - 'T' => OutputType::Str(pretty_fstype(meta.fs_type()).into()), - _ => OutputType::Unknown, - }; - - print_it(&output, flag, width, precision); - } - } + process_token_filesystem(t, meta, &display_name); } } Err(e) => { @@ -728,125 +976,15 @@ impl Stater { }; for t in tokens { - match *t { - Token::Char(c) => print!("{c}"), - Token::Directive { - flag, - width, - precision, - format, - } => { - let output = match format { - // access rights in octal - 'a' => OutputType::UnsignedOct(0o7777 & meta.mode()), - // access rights in human readable form - 'A' => OutputType::Str(display_permissions(&meta, true)), - // number of blocks allocated (see %B) - 'b' => OutputType::Unsigned(meta.blocks()), - - // the size in bytes of each block reported by %b - // FIXME: blocksize differs on various platform - // See coreutils/gnulib/lib/stat-size.h ST_NBLOCKSIZE // spell-checker:disable-line - 'B' => OutputType::Unsigned(512), - - // device number in decimal - 'd' => OutputType::Unsigned(meta.dev()), - // device number in hex - 'D' => OutputType::UnsignedHex(meta.dev()), - // raw mode in hex - 'f' => OutputType::UnsignedHex(meta.mode() as u64), - // file type - 'F' => OutputType::Str( - pretty_filetype(meta.mode() as mode_t, meta.len()) - .to_owned(), - ), - // group ID of owner - 'g' => OutputType::Unsigned(meta.gid() as u64), - // group name of owner - 'G' => { - let group_name = entries::gid2grp(meta.gid()) - .unwrap_or_else(|_| "UNKNOWN".to_owned()); - OutputType::Str(group_name) - } - // number of hard links - 'h' => OutputType::Unsigned(meta.nlink()), - // inode number - 'i' => OutputType::Unsigned(meta.ino()), - // mount point - 'm' => OutputType::Str(self.find_mount_point(&file).unwrap()), - // file name - 'n' => OutputType::Str(display_name.to_string()), - // quoted file name with dereference if symbolic link - 'N' => { - let file_name = if file_type.is_symlink() { - let dst = match fs::read_link(&file) { - Ok(path) => path, - Err(e) => { - println!("{e}"); - return 1; - } - }; - format!("{} -> {}", display_name.quote(), dst.quote()) - } else { - display_name.to_string() - }; - OutputType::Str(file_name) - } - // optimal I/O transfer size hint - 'o' => OutputType::Unsigned(meta.blksize()), - // total size, in bytes - 's' => OutputType::Integer(meta.len() as i64), - // major device type in hex, for character/block device special - // files - 't' => OutputType::UnsignedHex(meta.rdev() >> 8), - // minor device type in hex, for character/block device special - // files - 'T' => OutputType::UnsignedHex(meta.rdev() & 0xff), - // user ID of owner - 'u' => OutputType::Unsigned(meta.uid() as u64), - // user name of owner - 'U' => { - let user_name = entries::uid2usr(meta.uid()) - .unwrap_or_else(|_| "UNKNOWN".to_owned()); - OutputType::Str(user_name) - } - - // time of file birth, human-readable; - if unknown - 'w' => OutputType::Str( - meta.birth() - .map(|(sec, nsec)| pretty_time(sec as i64, nsec as i64)) - .unwrap_or(String::from("-")), - ), - - // time of file birth, seconds since Epoch; 0 if unknown - 'W' => OutputType::Unsigned(meta.birth().unwrap_or_default().0), - - // time of last access, human-readable - 'x' => OutputType::Str(pretty_time( - meta.atime(), - meta.atime_nsec(), - )), - // time of last access, seconds since Epoch - 'X' => OutputType::Integer(meta.atime()), - // time of last data modification, human-readable - 'y' => OutputType::Str(pretty_time( - meta.mtime(), - meta.mtime_nsec(), - )), - // time of last data modification, seconds since Epoch - 'Y' => OutputType::Integer(meta.mtime()), - // time of last status change, human-readable - 'z' => OutputType::Str(pretty_time( - meta.ctime(), - meta.ctime_nsec(), - )), - // time of last status change, seconds since Epoch - 'Z' => OutputType::Integer(meta.ctime()), - - _ => OutputType::Unknown, - }; - print_it(&output, flag, width, precision); - } + if let Err(code) = self.process_token_files( + t, + &meta, + &display_name, + &file, + &file_type, + self.from_user, + ) { + return code; } } } @@ -1038,7 +1176,7 @@ mod tests { #[test] fn printf_format() { - let s = r#"%-# 15a\t\r\"\\\a\b\e\f\v%+020.-23w\x12\167\132\112\n"#; + let s = r#"%-# 15a\t\r\"\\\a\b\x1B\f\x0B%+020.-23w\x12\167\132\112\n"#; let expected = vec![ Token::Directive { flag: Flags { @@ -1051,15 +1189,15 @@ mod tests { precision: None, format: 'a', }, - Token::Char('\t'), - Token::Char('\r'), - Token::Char('"'), - Token::Char('\\'), - Token::Char('\x07'), - Token::Char('\x08'), - Token::Char('\x1B'), - Token::Char('\x0C'), - Token::Char('\x0B'), + Token::Byte(b'\t'), + Token::Byte(b'\r'), + Token::Byte(b'"'), + Token::Byte(b'\\'), + Token::Byte(b'\x07'), + Token::Byte(b'\x08'), + Token::Byte(b'\x1B'), + Token::Byte(b'\x0C'), + Token::Byte(b'\x0B'), Token::Directive { flag: Flags { sign: true, @@ -1070,11 +1208,11 @@ mod tests { precision: None, format: 'w', }, - Token::Char('\x12'), - Token::Char('w'), - Token::Char('Z'), - Token::Char('J'), - Token::Char('\n'), + Token::Byte(b'\x12'), + Token::Byte(b'w'), + Token::Byte(b'Z'), + Token::Byte(b'J'), + Token::Byte(b'\n'), ]; assert_eq!(&expected, &Stater::generate_tokens(s, true).unwrap()); } diff --git a/src/uu/stdbuf/src/libstdbuf/Cargo.toml b/src/uu/stdbuf/src/libstdbuf/Cargo.toml index ff9de77fc..67a7e903e 100644 --- a/src/uu/stdbuf/src/libstdbuf/Cargo.toml +++ b/src/uu/stdbuf/src/libstdbuf/Cargo.toml @@ -20,8 +20,8 @@ crate-type = [ ] # XXX: note: the rlib is just to prevent Cargo from spitting out a warning [dependencies] -cpp = "0.5.9" +cpp = "0.5.10" libc = { workspace = true } [build-dependencies] -cpp_build = "0.5.9" +cpp_build = "0.5.10" diff --git a/src/uu/sum/src/sum.rs b/src/uu/sum/src/sum.rs index d1f383351..bae288d80 100644 --- a/src/uu/sum/src/sum.rs +++ b/src/uu/sum/src/sum.rs @@ -16,13 +16,6 @@ use uucore::{format_usage, help_about, help_usage, show}; const USAGE: &str = help_usage!("sum.md"); const ABOUT: &str = help_about!("sum.md"); -// This can be replaced with usize::div_ceil once it is stabilized. -// This implementation approach is optimized for when `b` is a constant, -// particularly a power of two. -const fn div_ceil(a: usize, b: usize) -> usize { - (a + b - 1) / b -} - fn bsd_sum(mut reader: Box) -> (usize, u16) { let mut buf = [0; 4096]; let mut bytes_read = 0; @@ -41,7 +34,7 @@ fn bsd_sum(mut reader: Box) -> (usize, u16) { } // Report blocks read in terms of 1024-byte blocks. - let blocks_read = div_ceil(bytes_read, 1024); + let blocks_read = bytes_read.div_ceil(1024); (blocks_read, checksum) } @@ -66,7 +59,7 @@ fn sysv_sum(mut reader: Box) -> (usize, u16) { ret = (ret & 0xffff) + (ret >> 16); // Report blocks read in terms of 512-byte blocks. - let blocks_read = div_ceil(bytes_read, 512); + let blocks_read = bytes_read.div_ceil(512); (blocks_read, ret as u16) } diff --git a/src/uu/tac/src/tac.rs b/src/uu/tac/src/tac.rs index 3865c61ae..d1eca4706 100644 --- a/src/uu/tac/src/tac.rs +++ b/src/uu/tac/src/tac.rs @@ -184,7 +184,7 @@ fn buffer_tac(data: &[u8], before: bool, separator: &str) -> std::io::Result<()> let mut out = BufWriter::new(out.lock()); // The number of bytes in the line separator. - let slen = separator.as_bytes().len(); + let slen = separator.len(); // The index of the start of the next line in the `data`. // diff --git a/src/uu/tail/src/args.rs b/src/uu/tail/src/args.rs index 5cadac608..24b064d1b 100644 --- a/src/uu/tail/src/args.rs +++ b/src/uu/tail/src/args.rs @@ -336,11 +336,11 @@ impl Settings { let blocking_stdin = self.pid == 0 && self.follow == Some(FollowMode::Descriptor) && self.num_inputs() == 1 - && Handle::stdin().map_or(false, |handle| { + && Handle::stdin().is_ok_and(|handle| { handle .as_file() .metadata() - .map_or(false, |meta| !meta.is_file()) + .is_ok_and(|meta| !meta.is_file()) }); if !blocking_stdin && std::io::stdin().is_terminal() { diff --git a/src/uu/tail/src/chunks.rs b/src/uu/tail/src/chunks.rs index 636de7a90..2c80ac0ac 100644 --- a/src/uu/tail/src/chunks.rs +++ b/src/uu/tail/src/chunks.rs @@ -64,7 +64,7 @@ impl<'a> ReverseChunks<'a> { } } -impl<'a> Iterator for ReverseChunks<'a> { +impl Iterator for ReverseChunks<'_> { type Item = Vec; fn next(&mut self) -> Option { diff --git a/src/uu/tail/src/paths.rs b/src/uu/tail/src/paths.rs index 117cab8b0..4a680943c 100644 --- a/src/uu/tail/src/paths.rs +++ b/src/uu/tail/src/paths.rs @@ -93,7 +93,7 @@ impl Input { pub fn is_tailable(&self) -> bool { match &self.kind { InputKind::File(path) => path_is_tailable(path), - InputKind::Stdin => self.resolve().map_or(false, |path| path_is_tailable(&path)), + InputKind::Stdin => self.resolve().is_some_and(|path| path_is_tailable(&path)), } } } @@ -233,7 +233,7 @@ impl PathExtTail for Path { } pub fn path_is_tailable(path: &Path) -> bool { - path.is_file() || path.exists() && path.metadata().map_or(false, |meta| meta.is_tailable()) + path.is_file() || path.exists() && path.metadata().is_ok_and(|meta| meta.is_tailable()) } #[inline] diff --git a/src/uu/tail/src/tail.rs b/src/uu/tail/src/tail.rs index edac4b151..a48da6b31 100644 --- a/src/uu/tail/src/tail.rs +++ b/src/uu/tail/src/tail.rs @@ -65,13 +65,15 @@ fn uu_tail(settings: &Settings) -> UResult<()> { // Add `path` and `reader` to `files` map if `--follow` is selected. for input in &settings.inputs.clone() { match input.kind() { - InputKind::File(path) if cfg!(not(unix)) || path != &PathBuf::from(text::DEV_STDIN) => { - tail_file(settings, &mut printer, input, path, &mut observer, 0)?; - } - // File points to /dev/stdin here - InputKind::File(_) | InputKind::Stdin => { + InputKind::Stdin => { tail_stdin(settings, &mut printer, input, &mut observer)?; } + InputKind::File(path) if cfg!(unix) && path == &PathBuf::from(text::DEV_STDIN) => { + tail_stdin(settings, &mut printer, input, &mut observer)?; + } + InputKind::File(path) => { + tail_file(settings, &mut printer, input, path, &mut observer, 0)?; + } } } @@ -85,7 +87,7 @@ fn uu_tail(settings: &Settings) -> UResult<()> { the input file is not a FIFO, pipe, or regular file, it is unspecified whether or not the -f option shall be ignored. */ - if !settings.has_only_stdin() { + if !settings.has_only_stdin() || settings.pid != 0 { follow::follow(observer, settings)?; } } diff --git a/src/uu/timeout/src/timeout.rs b/src/uu/timeout/src/timeout.rs index 19016900a..2ba93769a 100644 --- a/src/uu/timeout/src/timeout.rs +++ b/src/uu/timeout/src/timeout.rs @@ -288,7 +288,6 @@ fn preserve_signal_info(signal: libc::c_int) -> libc::c_int { } /// TODO: Improve exit codes, and make them consistent with the GNU Coreutils exit codes. - fn timeout( cmd: &[String], duration: Duration, diff --git a/src/uu/tr/Cargo.toml b/src/uu/tr/Cargo.toml index 0787e4279..6378b7766 100644 --- a/src/uu/tr/Cargo.toml +++ b/src/uu/tr/Cargo.toml @@ -19,7 +19,7 @@ path = "src/tr.rs" [dependencies] nom = { workspace = true } clap = { workspace = true } -uucore = { workspace = true } +uucore = { workspace = true, features = ["fs"] } [[bin]] name = "tr" diff --git a/src/uu/tr/src/operation.rs b/src/uu/tr/src/operation.rs index fc01a8360..6a1bf9391 100644 --- a/src/uu/tr/src/operation.rs +++ b/src/uu/tr/src/operation.rs @@ -16,13 +16,15 @@ use nom::{ IResult, }; use std::{ + char, collections::{HashMap, HashSet}, error::Error, fmt::{Debug, Display}, io::{BufRead, Write}, ops::Not, }; -use uucore::error::UError; +use uucore::error::{UError, UResult, USimpleError}; +use uucore::show_warning; #[derive(Debug, Clone)] pub enum BadSequence { @@ -293,7 +295,9 @@ impl Sequence { Self::parse_class, Self::parse_char_equal, // NOTE: This must be the last one - map(Self::parse_backslash_or_char, |s| Ok(Self::Char(s))), + map(Self::parse_backslash_or_char_with_warning, |s| { + Ok(Self::Char(s)) + }), )))(input) .map(|(_, r)| r) .unwrap() @@ -302,10 +306,16 @@ impl Sequence { } fn parse_octal(input: &[u8]) -> IResult<&[u8], u8> { + // For `parse_char_range`, `parse_char_star`, `parse_char_repeat`, `parse_char_equal`. + // Because in these patterns, there's no ambiguous cases. + preceded(tag("\\"), Self::parse_octal_up_to_three_digits)(input) + } + + fn parse_octal_with_warning(input: &[u8]) -> IResult<&[u8], u8> { preceded( tag("\\"), alt(( - Self::parse_octal_up_to_three_digits, + Self::parse_octal_up_to_three_digits_with_warning, // Fallback for if the three digit octal escape is greater than \377 (0xFF), and therefore can't be // parsed as as a byte // See test `test_multibyte_octal_sequence` @@ -319,16 +329,29 @@ impl Sequence { recognize(many_m_n(1, 3, one_of("01234567"))), |out: &[u8]| { let str_to_parse = std::str::from_utf8(out).unwrap(); + u8::from_str_radix(str_to_parse, 8).ok() + }, + )(input) + } - match u8::from_str_radix(str_to_parse, 8) { - Ok(ue) => Some(ue), - Err(_pa) => { - // TODO - // A warning needs to be printed here - // See https://github.com/uutils/coreutils/issues/6821 - None - } + fn parse_octal_up_to_three_digits_with_warning(input: &[u8]) -> IResult<&[u8], u8> { + map_opt( + recognize(many_m_n(1, 3, one_of("01234567"))), + |out: &[u8]| { + let str_to_parse = std::str::from_utf8(out).unwrap(); + let result = u8::from_str_radix(str_to_parse, 8).ok(); + if result.is_none() { + let origin_octal: &str = std::str::from_utf8(input).unwrap(); + let actual_octal_tail: &str = std::str::from_utf8(&input[0..2]).unwrap(); + let outstand_char: char = char::from_u32(input[2] as u32).unwrap(); + show_warning!( + "the ambiguous octal escape \\{} is being\n interpreted as the 2-byte sequence \\0{}, {}", + origin_octal, + actual_octal_tail, + outstand_char + ); } + result }, )(input) } @@ -360,6 +383,14 @@ impl Sequence { alt((Self::parse_octal, Self::parse_backslash, Self::single_char))(input) } + fn parse_backslash_or_char_with_warning(input: &[u8]) -> IResult<&[u8], u8> { + alt(( + Self::parse_octal_with_warning, + Self::parse_backslash, + Self::single_char, + ))(input) + } + fn single_char(input: &[u8]) -> IResult<&[u8], u8> { take(1usize)(input).map(|(l, a)| (l, a[0])) } @@ -577,7 +608,7 @@ impl SymbolTranslator for SqueezeOperation { } } -pub fn translate_input(input: &mut R, output: &mut W, mut translator: T) +pub fn translate_input(input: &mut R, output: &mut W, mut translator: T) -> UResult<()> where T: SymbolTranslator, R: BufRead, @@ -585,15 +616,25 @@ where { let mut buf = Vec::new(); let mut output_buf = Vec::new(); + while let Ok(length) = input.read_until(b'\n', &mut buf) { if length == 0 { - break; - } else { - let filtered = buf.iter().filter_map(|c| translator.translate(*c)); - output_buf.extend(filtered); - output.write_all(&output_buf).unwrap(); + break; // EOF reached } + + let filtered = buf.iter().filter_map(|&c| translator.translate(c)); + output_buf.extend(filtered); + + if let Err(e) = output.write_all(&output_buf) { + return Err(USimpleError::new( + 1, + format!("{}: write error: {}", uucore::util_name(), e), + )); + } + buf.clear(); output_buf.clear(); } + + Ok(()) } diff --git a/src/uu/tr/src/tr.rs b/src/uu/tr/src/tr.rs index 67998d26d..c226d2189 100644 --- a/src/uu/tr/src/tr.rs +++ b/src/uu/tr/src/tr.rs @@ -8,17 +8,17 @@ mod operation; mod unicode_table; +use crate::operation::DeleteOperation; use clap::{crate_version, value_parser, Arg, ArgAction, Command}; use operation::{ translate_input, Sequence, SqueezeOperation, SymbolTranslator, TranslateOperation, }; use std::ffi::OsString; use std::io::{stdin, stdout, BufWriter}; -use uucore::{format_usage, help_about, help_section, help_usage, os_str_as_bytes, show}; - -use crate::operation::DeleteOperation; use uucore::display::Quotable; use uucore::error::{UResult, USimpleError, UUsageError}; +use uucore::fs::is_stdin_directory; +use uucore::{format_usage, help_about, help_section, help_usage, os_str_as_bytes, show}; const ABOUT: &str = help_about!("tr.md"); const AFTER_HELP: &str = help_section!("after help", "tr.md"); @@ -126,30 +126,34 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { translating, )?; + if is_stdin_directory(&stdin) { + return Err(USimpleError::new(1, "read error: Is a directory")); + } + // '*_op' are the operations that need to be applied, in order. if delete_flag { if squeeze_flag { let delete_op = DeleteOperation::new(set1); let squeeze_op = SqueezeOperation::new(set2); let op = delete_op.chain(squeeze_op); - translate_input(&mut locked_stdin, &mut buffered_stdout, op); + translate_input(&mut locked_stdin, &mut buffered_stdout, op)?; } else { let op = DeleteOperation::new(set1); - translate_input(&mut locked_stdin, &mut buffered_stdout, op); + translate_input(&mut locked_stdin, &mut buffered_stdout, op)?; } } else if squeeze_flag { if sets_len < 2 { let op = SqueezeOperation::new(set1); - translate_input(&mut locked_stdin, &mut buffered_stdout, op); + translate_input(&mut locked_stdin, &mut buffered_stdout, op)?; } else { let translate_op = TranslateOperation::new(set1, set2.clone())?; let squeeze_op = SqueezeOperation::new(set2); let op = translate_op.chain(squeeze_op); - translate_input(&mut locked_stdin, &mut buffered_stdout, op); + translate_input(&mut locked_stdin, &mut buffered_stdout, op)?; } } else { let op = TranslateOperation::new(set1, set2)?; - translate_input(&mut locked_stdin, &mut buffered_stdout, op); + translate_input(&mut locked_stdin, &mut buffered_stdout, op)?; } Ok(()) } diff --git a/src/uu/uniq/src/uniq.rs b/src/uu/uniq/src/uniq.rs index 4084a7b3f..b9090cd50 100644 --- a/src/uu/uniq/src/uniq.rs +++ b/src/uu/uniq/src/uniq.rs @@ -383,7 +383,7 @@ fn should_extract_obs_skip_chars( && posix_version().is_some_and(|v| v <= OBSOLETE) && !preceding_long_opt_req_value && !preceding_short_opt_req_value - && slice.chars().nth(1).map_or(false, |c| c.is_ascii_digit()) + && slice.chars().nth(1).is_some_and(|c| c.is_ascii_digit()) } /// Helper function to [`filter_args`] diff --git a/src/uu/wc/src/utf8/read.rs b/src/uu/wc/src/utf8/read.rs index 819b0a689..9515cdc9f 100644 --- a/src/uu/wc/src/utf8/read.rs +++ b/src/uu/wc/src/utf8/read.rs @@ -27,7 +27,7 @@ pub enum BufReadDecoderError<'a> { Io(io::Error), } -impl<'a> fmt::Display for BufReadDecoderError<'a> { +impl fmt::Display for BufReadDecoderError<'_> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { BufReadDecoderError::InvalidByteSequence(bytes) => { @@ -38,7 +38,7 @@ impl<'a> fmt::Display for BufReadDecoderError<'a> { } } -impl<'a> Error for BufReadDecoderError<'a> { +impl Error for BufReadDecoderError<'_> { fn source(&self) -> Option<&(dyn Error + 'static)> { match *self { BufReadDecoderError::InvalidByteSequence(_) => None, diff --git a/src/uu/wc/src/wc.rs b/src/uu/wc/src/wc.rs index 33b70ee62..1c2d99628 100644 --- a/src/uu/wc/src/wc.rs +++ b/src/uu/wc/src/wc.rs @@ -13,7 +13,7 @@ mod word_count; use std::{ borrow::{Borrow, Cow}, cmp::max, - ffi::OsString, + ffi::{OsStr, OsString}, fs::{self, File}, io::{self, Write}, iter, @@ -28,7 +28,7 @@ use utf8::{BufReadDecoder, BufReadDecoderError}; use uucore::{ error::{FromIo, UError, UResult}, format_usage, help_about, help_usage, - quoting_style::{escape_name, QuotingStyle}, + quoting_style::{self, QuotingStyle}, shortcut_value_parser::ShortcutValueParser, show, }; @@ -259,7 +259,7 @@ impl<'a> Input<'a> { match self { Self::Path(path) => Some(match path.to_str() { Some(s) if !s.contains('\n') => Cow::Borrowed(s), - _ => Cow::Owned(escape_name(path.as_os_str(), QS_ESCAPE)), + _ => Cow::Owned(escape_name_wrapper(path.as_os_str())), }), Self::Stdin(StdinKind::Explicit) => Some(Cow::Borrowed(STDIN_REPR)), Self::Stdin(StdinKind::Implicit) => None, @@ -269,7 +269,7 @@ impl<'a> Input<'a> { /// Converts input into the form that appears in errors. fn path_display(&self) -> String { match self { - Self::Path(path) => escape_name(path.as_os_str(), QS_ESCAPE), + Self::Path(path) => escape_name_wrapper(path.as_os_str()), Self::Stdin(_) => String::from("standard input"), } } @@ -361,7 +361,7 @@ impl WcError { Some((input, idx)) => { let path = match input { Input::Stdin(_) => STDIN_REPR.into(), - Input::Path(path) => escape_name(path.as_os_str(), QS_ESCAPE).into(), + Input::Path(path) => escape_name_wrapper(path.as_os_str()).into(), }; Self::ZeroLengthFileNameCtx { path, idx } } @@ -761,7 +761,9 @@ fn files0_iter_file<'a>(path: &Path) -> UResult Err(e.map_err_context(|| { format!( "cannot open {} for reading", - escape_name(path.as_os_str(), QS_QUOTE_ESCAPE) + quoting_style::escape_name(path.as_os_str(), QS_QUOTE_ESCAPE) + .into_string() + .expect("All escaped names with the escaping option return valid strings.") ) })), } @@ -793,9 +795,9 @@ fn files0_iter<'a>( Ok(Input::Path(PathBuf::from(s).into())) } } - Err(e) => Err(e.map_err_context(|| { - format!("{}: read error", escape_name(&err_path, QS_ESCAPE)) - }) as Box), + Err(e) => Err(e + .map_err_context(|| format!("{}: read error", escape_name_wrapper(&err_path))) + as Box), }), ); // Loop until there is an error; yield that error and then nothing else. @@ -808,6 +810,12 @@ fn files0_iter<'a>( }) } +fn escape_name_wrapper(name: &OsStr) -> String { + quoting_style::escape_name(name, QS_ESCAPE) + .into_string() + .expect("All escaped names with the escaping option return valid strings.") +} + fn wc(inputs: &Inputs, settings: &Settings) -> UResult<()> { let mut total_word_count = WordCount::default(); let mut num_inputs: usize = 0; diff --git a/src/uucore/Cargo.toml b/src/uucore/Cargo.toml index b72a8ed71..5e1a065a6 100644 --- a/src/uucore/Cargo.toml +++ b/src/uucore/Cargo.toml @@ -25,6 +25,7 @@ dns-lookup = { workspace = true, optional = true } dunce = { version = "1.0.4", optional = true } wild = "2.2.1" glob = { workspace = true } +lazy_static = "1.4.0" # * optional itertools = { workspace = true, optional = true } thiserror = { workspace = true, optional = true } @@ -86,6 +87,7 @@ lines = [] format = ["itertools", "quoting-style"] mode = ["libc"] perms = ["libc", "walkdir"] +buf-copy = [] pipes = [] process = ["libc"] proc-info = ["tty", "walkdir"] diff --git a/src/uucore/src/lib/features.rs b/src/uucore/src/lib/features.rs index cf24637f7..cde1cf264 100644 --- a/src/uucore/src/lib/features.rs +++ b/src/uucore/src/lib/features.rs @@ -39,11 +39,13 @@ pub mod version_cmp; pub mod mode; // ** unix-only +#[cfg(all(any(target_os = "linux", target_os = "android"), feature = "buf-copy"))] +pub mod buf_copy; #[cfg(all(unix, feature = "entries"))] pub mod entries; #[cfg(all(unix, feature = "perms"))] pub mod perms; -#[cfg(all(unix, feature = "pipes"))] +#[cfg(all(unix, any(feature = "pipes", feature = "buf-copy")))] pub mod pipes; #[cfg(all(target_os = "linux", feature = "proc-info"))] pub mod proc_info; @@ -52,7 +54,7 @@ pub mod process; #[cfg(all(target_os = "linux", feature = "tty"))] pub mod tty; -#[cfg(all(unix, not(target_os = "macos"), feature = "fsxattr"))] +#[cfg(all(unix, feature = "fsxattr"))] pub mod fsxattr; #[cfg(all(unix, not(target_os = "fuchsia"), feature = "signals"))] pub mod signals; diff --git a/src/uucore/src/lib/features/backup_control.rs b/src/uucore/src/lib/features/backup_control.rs index 9086acb19..4b4f7aa93 100644 --- a/src/uucore/src/lib/features/backup_control.rs +++ b/src/uucore/src/lib/features/backup_control.rs @@ -421,25 +421,29 @@ pub fn get_backup_path( } fn simple_backup_path(path: &Path, suffix: &str) -> PathBuf { - let mut p = path.to_string_lossy().into_owned(); - p.push_str(suffix); - PathBuf::from(p) + let mut file_name = path.file_name().unwrap_or_default().to_os_string(); + file_name.push(suffix); + path.with_file_name(file_name) } fn numbered_backup_path(path: &Path) -> PathBuf { + let file_name = path.file_name().unwrap_or_default(); for i in 1_u64.. { - let path_str = &format!("{}.~{}~", path.to_string_lossy(), i); - let path = Path::new(path_str); + let mut numbered_file_name = file_name.to_os_string(); + numbered_file_name.push(format!(".~{}~", i)); + let path = path.with_file_name(numbered_file_name); if !path.exists() { - return path.to_path_buf(); + return path; } } panic!("cannot create backup") } fn existing_backup_path(path: &Path, suffix: &str) -> PathBuf { - let test_path_str = &format!("{}.~1~", path.to_string_lossy()); - let test_path = Path::new(test_path_str); + let file_name = path.file_name().unwrap_or_default(); + let mut numbered_file_name = file_name.to_os_string(); + numbered_file_name.push(".~1~"); + let test_path = path.with_file_name(numbered_file_name); if test_path.exists() { numbered_backup_path(path) } else { @@ -660,6 +664,44 @@ mod tests { let result = determine_backup_suffix(&matches); assert_eq!(result, "-v"); } + + #[test] + fn test_numbered_backup_path() { + assert_eq!(numbered_backup_path(&Path::new("")), PathBuf::from(".~1~")); + assert_eq!( + numbered_backup_path(&Path::new("/")), + PathBuf::from("/.~1~") + ); + assert_eq!( + numbered_backup_path(&Path::new("/hello/world")), + PathBuf::from("/hello/world.~1~") + ); + assert_eq!( + numbered_backup_path(&Path::new("/hello/world/")), + PathBuf::from("/hello/world.~1~") + ); + } + + #[test] + fn test_simple_backup_path() { + assert_eq!( + simple_backup_path(&Path::new(""), ".bak"), + PathBuf::from(".bak") + ); + assert_eq!( + simple_backup_path(&Path::new("/"), ".bak"), + PathBuf::from("/.bak") + ); + assert_eq!( + simple_backup_path(&Path::new("/hello/world"), ".bak"), + PathBuf::from("/hello/world.bak") + ); + assert_eq!( + simple_backup_path(&Path::new("/hello/world/"), ".bak"), + PathBuf::from("/hello/world.bak") + ); + } + #[test] fn test_source_is_target_backup() { let source = Path::new("data.txt.bak"); diff --git a/src/uucore/src/lib/features/buf_copy.rs b/src/uucore/src/lib/features/buf_copy.rs new file mode 100644 index 000000000..2b46248a5 --- /dev/null +++ b/src/uucore/src/lib/features/buf_copy.rs @@ -0,0 +1,373 @@ +// 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. + +//! This module provides several buffer-based copy/write functions that leverage +//! the `splice` system call in Linux systems, thus increasing the I/O +//! performance of copying between two file descriptors. This module is mostly +//! used by utilities to work around the limitations of Rust's `fs::copy` which +//! does not handle copying special files (e.g pipes, character/block devices). + +use crate::error::{UError, UResult}; +use nix::unistd; +use std::fs::File; +use std::{ + io::{self, Read, Write}, + os::{ + fd::AsFd, + unix::io::{AsRawFd, RawFd}, + }, +}; + +use nix::{errno::Errno, libc::S_IFIFO, sys::stat::fstat}; + +use super::pipes::{pipe, splice, splice_exact, vmsplice}; + +type Result = std::result::Result; + +/// Error types used by buffer-copying functions from the `buf_copy` module. +#[derive(Debug)] +pub enum Error { + Io(io::Error), + WriteError(String), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::WriteError(msg) => write!(f, "splice() write error: {}", msg), + Error::Io(err) => write!(f, "I/O error: {}", err), + } + } +} + +impl std::error::Error for Error {} + +impl UError for Error { + fn code(&self) -> i32 { + 1 + } + + fn usage(&self) -> bool { + false + } +} + +/// Helper function to determine whether a given handle (such as a file) is a pipe or not. +/// +/// # Arguments +/// * `out` - path of handle +/// +/// # Returns +/// A `bool` indicating whether the given handle is a pipe or not. +#[inline] +#[cfg(unix)] +pub fn is_pipe

(path: &P) -> Result +where + P: AsRawFd, +{ + Ok(fstat(path.as_raw_fd())?.st_mode as nix::libc::mode_t & S_IFIFO != 0) +} + +const SPLICE_SIZE: usize = 1024 * 128; +const BUF_SIZE: usize = 1024 * 16; + +/// Copy data from `Read` implementor `source` into a `Write` implementor +/// `dest`. This works by reading a chunk of data from `source` and writing the +/// data to `dest` in a loop. +/// +/// This function uses the Linux-specific `splice` call when possible which does +/// not use any intermediate user-space buffer. It falls backs to +/// `std::io::copy` under other platforms or when the call fails and is still +/// recoverable. +/// +/// # Arguments +/// * `source` - `Read` implementor to copy data from. +/// * `dest` - `Write` implementor to copy data to. +/// +/// # Returns +/// +/// Result of operation and bytes successfully written (as a `u64`) when +/// operation is successful. +pub fn copy_stream(src: &mut R, dest: &mut S) -> UResult +where + R: Read + AsFd + AsRawFd, + S: Write + AsFd + AsRawFd, +{ + #[cfg(any(target_os = "linux", target_os = "android"))] + { + // If we're on Linux or Android, try to use the splice() system call + // for faster writing. If it works, we're done. + let result = splice_write(src, &dest.as_fd())?; + if !result.1 { + return Ok(result.0); + } + } + // If we're not on Linux or Android, or the splice() call failed, + // fall back on slower writing. + let result = std::io::copy(src, dest)?; + + // If the splice() call failed and there has been some data written to + // stdout via while loop above AND there will be second splice() call + // that will succeed, data pushed through splice will be output before + // the data buffered in stdout.lock. Therefore additional explicit flush + // is required here. + dest.flush()?; + Ok(result) +} + +/// Write from source `handle` into destination `write_fd` using Linux-specific +/// `splice` system call. +/// +/// # Arguments +/// - `source` - source handle +/// - `dest` - destination handle +#[inline] +#[cfg(any(target_os = "linux", target_os = "android"))] +fn splice_write(source: &R, dest: &S) -> UResult<(u64, bool)> +where + R: Read + AsFd + AsRawFd, + S: AsRawFd + AsFd, +{ + let (pipe_rd, pipe_wr) = pipe()?; + let mut bytes: u64 = 0; + + loop { + match splice(&source, &pipe_wr, SPLICE_SIZE) { + Ok(n) => { + if n == 0 { + return Ok((bytes, false)); + } + if splice_exact(&pipe_rd, dest, n).is_err() { + // If the first splice manages to copy to the intermediate + // pipe, but the second splice to stdout fails for some reason + // we can recover by copying the data that we have from the + // intermediate pipe to stdout using normal read/write. Then + // we tell the caller to fall back. + copy_exact(pipe_rd.as_raw_fd(), dest, n)?; + return Ok((bytes, true)); + } + + bytes += n as u64; + } + Err(_) => { + return Ok((bytes, true)); + } + } + } +} + +/// Move exactly `num_bytes` bytes from `read_fd` to `write_fd` using the `read` +/// and `write` calls. +fn copy_exact(read_fd: RawFd, write_fd: &impl AsFd, num_bytes: usize) -> std::io::Result { + let mut left = num_bytes; + let mut buf = [0; BUF_SIZE]; + let mut written = 0; + while left > 0 { + let read = unistd::read(read_fd, &mut buf)?; + assert_ne!(read, 0, "unexpected end of pipe"); + while written < read { + let n = unistd::write(write_fd, &buf[written..read])?; + written += n; + } + left -= read; + } + Ok(written) +} + +/// Write input `bytes` to a file descriptor. This uses the Linux-specific +/// `vmsplice()` call to write into a file descriptor directly, which only works +/// if the destination is a pipe. +/// +/// # Arguments +/// * `bytes` - data to be written +/// * `dest` - destination handle +/// +/// # Returns +/// When write succeeds, the amount of bytes written is returned as a +/// `u64`. The `bool` indicates if we need to fall back to normal copying or +/// not. `true` means we need to fall back, `false` means we don't have to. +/// +/// A `UError` error is returned when the operation is not supported or when an +/// I/O error occurs. +#[cfg(any(target_os = "linux", target_os = "android"))] +pub fn splice_data_to_pipe(bytes: &[u8], dest: &T) -> UResult<(u64, bool)> +where + T: AsRawFd + AsFd, +{ + let mut n_bytes: u64 = 0; + let mut bytes = bytes; + while !bytes.is_empty() { + let len = match vmsplice(dest, bytes) { + Ok(n) => n, + // The maybe_unsupported call below may emit an error, when the + // error is considered as unrecoverable error (ones that won't make + // us fall back to other method) + Err(e) => return Ok(maybe_unsupported(e)?), + }; + bytes = &bytes[len..]; + n_bytes += len as u64; + } + Ok((n_bytes, false)) +} + +/// Write input `bytes` to a handle using a temporary pipe. A `vmsplice()` call +/// is issued to write to the temporary pipe, which then gets written to the +/// final destination using `splice()`. +/// +/// # Arguments * `bytes` - data to be written * `dest` - destination handle +/// +/// # Returns When write succeeds, the amount of bytes written is returned as a +/// `u64`. The `bool` indicates if we need to fall back to normal copying or +/// not. `true` means we need to fall back, `false` means we don't have to. +/// +/// A `UError` error is returned when the operation is not supported or when an +/// I/O error occurs. +#[cfg(any(target_os = "linux", target_os = "android"))] +pub fn splice_data_to_fd( + bytes: &[u8], + read_pipe: &File, + write_pipe: &File, + dest: &T, +) -> UResult<(u64, bool)> { + loop { + let mut bytes = bytes; + while !bytes.is_empty() { + let len = match vmsplice(&write_pipe, bytes) { + Ok(n) => n, + Err(e) => return Ok(maybe_unsupported(e)?), + }; + if let Err(e) = splice_exact(&read_pipe, dest, len) { + return Ok(maybe_unsupported(e)?); + } + bytes = &bytes[len..]; + } + } +} + +/// Conversion from a `nix::Error` into our `Error` which implements `UError`. +#[cfg(any(target_os = "linux", target_os = "android"))] +impl From for Error { + fn from(error: nix::Error) -> Self { + Self::Io(io::Error::from_raw_os_error(error as i32)) + } +} + +/// Several error values from `nix::Error` (`EINVAL`, `ENOSYS`, and `EBADF`) get +/// treated as errors indicating that the `splice` call is not available, i.e we +/// can still recover from the error. Thus, return the final result of the call +/// as `Result` and indicate that we have to fall back using other write method. +/// +/// # Arguments +/// * `error` - the `nix::Error` received +/// +/// # Returns +/// Result with tuple containing a `u64` `0` indicating that no data had been +/// written and a `true` indicating we have to fall back, if error is still +/// recoverable. Returns an `Error` implementing `UError` otherwise. +#[cfg(any(target_os = "linux", target_os = "android"))] +fn maybe_unsupported(error: nix::Error) -> Result<(u64, bool)> { + match error { + Errno::EINVAL | Errno::ENOSYS | Errno::EBADF => Ok((0, true)), + _ => Err(error.into()), + } +} + +#[cfg(test)] +mod tests { + use tempfile::tempdir; + + use super::*; + use crate::pipes; + + fn new_temp_file() -> File { + let temp_dir = tempdir().unwrap(); + File::create(temp_dir.path().join("file.txt")).unwrap() + } + + #[test] + fn test_file_is_pipe() { + let temp_file = new_temp_file(); + let (pipe_read, pipe_write) = pipes::pipe().unwrap(); + + assert!(is_pipe(&pipe_read).unwrap()); + assert!(is_pipe(&pipe_write).unwrap()); + assert!(!is_pipe(&temp_file).unwrap()); + } + + #[test] + fn test_valid_splice_errs() { + let err = nix::Error::from(Errno::EINVAL); + assert_eq!(maybe_unsupported(err).unwrap(), (0, true)); + + let err = nix::Error::from(Errno::ENOSYS); + assert_eq!(maybe_unsupported(err).unwrap(), (0, true)); + + let err = nix::Error::from(Errno::EBADF); + assert_eq!(maybe_unsupported(err).unwrap(), (0, true)); + + let err = nix::Error::from(Errno::EPERM); + assert!(maybe_unsupported(err).is_err()); + } + + #[test] + fn test_splice_data_to_pipe() { + let (pipe_read, pipe_write) = pipes::pipe().unwrap(); + let data = b"Hello, world!"; + let (bytes, _) = splice_data_to_pipe(data, &pipe_write).unwrap(); + let mut buf = [0; 1024]; + let n = unistd::read(pipe_read.as_raw_fd(), &mut buf).unwrap(); + assert_eq!(&buf[..n], data); + assert_eq!(bytes as usize, data.len()); + } + + #[test] + fn test_splice_data_to_file() { + let mut temp_file = new_temp_file(); + let (pipe_read, pipe_write) = pipes::pipe().unwrap(); + let data = b"Hello, world!"; + let (bytes, _) = splice_data_to_fd(data, &pipe_read, &pipe_write, &temp_file).unwrap(); + let mut buf = [0; 1024]; + let n = temp_file.read(&mut buf).unwrap(); + assert_eq!(&buf[..n], data); + assert_eq!(bytes as usize, data.len()); + } + + #[test] + fn test_copy_exact() { + let (mut pipe_read, mut pipe_write) = pipes::pipe().unwrap(); + let data = b"Hello, world!"; + let n = pipe_write.write(data).unwrap(); + assert_eq!(n, data.len()); + let mut buf = [0; 1024]; + let n = copy_exact(pipe_read.as_raw_fd(), &pipe_write, data.len()).unwrap(); + let n2 = pipe_read.read(&mut buf).unwrap(); + assert_eq!(n, n2); + assert_eq!(&buf[..n], data); + } + + #[test] + fn test_copy_stream() { + let (mut pipe_read, mut pipe_write) = pipes::pipe().unwrap(); + let data = b"Hello, world!"; + let n = pipe_write.write(data).unwrap(); + assert_eq!(n, data.len()); + let mut buf = [0; 1024]; + let n = copy_stream(&mut pipe_read, &mut pipe_write).unwrap(); + let n2 = pipe_read.read(&mut buf).unwrap(); + assert_eq!(n as usize, n2); + assert_eq!(&buf[..n as usize], data); + } + + #[test] + fn test_splice_write() { + let (mut pipe_read, pipe_write) = pipes::pipe().unwrap(); + let data = b"Hello, world!"; + let (bytes, _) = splice_write(&pipe_read, &pipe_write).unwrap(); + let mut buf = [0; 1024]; + let n = pipe_read.read(&mut buf).unwrap(); + assert_eq!(&buf[..n], data); + assert_eq!(bytes as usize, data.len()); + } +} diff --git a/src/uucore/src/lib/features/checksum.rs b/src/uucore/src/lib/features/checksum.rs index a2de28bc5..0b3e4e249 100644 --- a/src/uucore/src/lib/features/checksum.rs +++ b/src/uucore/src/lib/features/checksum.rs @@ -2,13 +2,15 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore anotherfile invalidchecksum regexes JWZG FFFD xffname prefixfilename +// spell-checker:ignore anotherfile invalidchecksum regexes JWZG FFFD xffname prefixfilename bytelen bitlen hexdigit use data_encoding::BASE64; +use lazy_static::lazy_static; use os_display::Quotable; -use regex::bytes::{Captures, Regex}; +use regex::bytes::{Match, Regex}; use std::{ - ffi::{OsStr, OsString}, + borrow::Cow, + ffi::OsStr, fmt::Display, fs::File, io::{self, stdin, BufReader, Read, Write}, @@ -68,11 +70,91 @@ pub struct HashAlgorithm { pub bits: usize, } +/// This structure holds the count of checksum test lines' outcomes. #[derive(Default)] struct ChecksumResult { - pub bad_format: i32, - pub failed_cksum: i32, - pub failed_open_file: i32, + /// Number of lines in the file where the computed checksum MATCHES + /// the expectation. + pub correct: u32, + /// Number of lines in the file where the computed checksum DIFFERS + /// from the expectation. + pub failed_cksum: u32, + pub failed_open_file: u32, + /// Number of improperly formatted lines. + pub bad_format: u32, + /// Total number of non-empty, non-comment lines. + pub total: u32, +} + +impl ChecksumResult { + #[inline] + fn total_properly_formatted(&self) -> u32 { + self.total - self.bad_format + } +} + +/// Represents a reason for which the processing of a checksum line +/// could not proceed to digest comparison. +enum LineCheckError { + /// a generic UError was encountered in sub-functions + UError(Box), + /// the computed checksum digest differs from the expected one + DigestMismatch, + /// the line is empty or is a comment + Skipped, + /// the line has a formatting error + ImproperlyFormatted, + /// file exists but is impossible to read + CantOpenFile, + /// there is nothing at the given path + FileNotFound, + /// the given path leads to a directory + FileIsDirectory, +} + +impl From> for LineCheckError { + fn from(value: Box) -> Self { + Self::UError(value) + } +} + +impl From for LineCheckError { + fn from(value: ChecksumError) -> Self { + Self::UError(Box::new(value)) + } +} + +/// Represents an error that was encountered when processing a checksum file. +enum FileCheckError { + /// a generic UError was encountered in sub-functions + UError(Box), + /// the checksum file is improperly formatted. + ImproperlyFormatted, + /// reading of the checksum file failed + CantOpenChecksumFile, +} + +impl From> for FileCheckError { + fn from(value: Box) -> Self { + Self::UError(value) + } +} + +impl From for FileCheckError { + fn from(value: ChecksumError) -> Self { + Self::UError(Box::new(value)) + } +} + +/// This struct regroups CLI flags. +#[derive(Debug, Default, Clone, Copy)] +pub struct ChecksumOptions { + pub binary: bool, + pub ignore_missing: bool, + pub quiet: bool, + pub status: bool, + pub strict: bool, + pub warn: bool, } #[derive(Debug, Error)] @@ -107,8 +189,6 @@ pub enum ChecksumError { CombineMultipleAlgorithms, #[error("Needs an algorithm to hash with.\nUse --help for more information.")] NeedAlgorithmToHash, - #[error("{filename}: no properly formatted checksum lines found")] - NoProperlyFormattedChecksumLinesFound { filename: String }, } impl UError for ChecksumError { @@ -174,6 +254,14 @@ fn cksum_output(res: &ChecksumResult, status: bool) { } } +/// Print a "no properly formatted lines" message in stderr +#[inline] +fn log_no_properly_formatted(filename: String) { + show_error!("{filename}: no properly formatted checksum lines found"); +} + +/// Represents the different outcomes that can happen to a file +/// that is being checked. #[derive(Debug, Clone, Copy)] enum FileChecksumResult { Ok, @@ -181,6 +269,28 @@ enum FileChecksumResult { CantOpen, } +impl FileChecksumResult { + /// Creates a `FileChecksumResult` from a digest comparison that + /// either succeeded or failed. + fn from_bool(checksum_correct: bool) -> Self { + if checksum_correct { + FileChecksumResult::Ok + } else { + FileChecksumResult::Failed + } + } + + /// The cli options might prevent to display on the outcome of the + /// comparison on STDOUT. + fn can_display(&self, opts: ChecksumOptions) -> bool { + match self { + FileChecksumResult::Ok => !opts.status && !opts.quiet, + FileChecksumResult::Failed => !opts.status, + FileChecksumResult::CantOpen => true, + } + } +} + impl Display for FileChecksumResult { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -198,10 +308,13 @@ fn print_file_report( filename: &[u8], result: FileChecksumResult, prefix: &str, + opts: ChecksumOptions, ) { - let _ = write!(w, "{prefix}"); - let _ = w.write_all(filename); - let _ = writeln!(w, ": {result}"); + if result.can_display(opts) { + let _ = write!(w, "{prefix}"); + let _ = w.write_all(filename); + let _ = writeln!(w, ": {result}"); + } } pub fn detect_algo(algo: &str, length: Option) -> UResult { @@ -308,14 +421,101 @@ pub fn detect_algo(algo: &str, length: Option) -> UResult // algo must be uppercase or b (for blake2b) // 2. [* ] // 3. [*] (only one space) -const ALGO_BASED_REGEX: &str = r"^\s*\\?(?P(?:[A-Z0-9]+|BLAKE2b))(?:-(?P\d+))?\s?\((?P(?-u:.*))\)\s*=\s*(?P[a-fA-F0-9]+)$"; -const ALGO_BASED_REGEX_BASE64: &str = r"^\s*\\?(?P(?:[A-Z0-9]+|BLAKE2b))(?:-(?P\d+))?\s?\((?P(?-u:.*))\)\s*=\s*(?P[A-Za-z0-9+/]+={0,2})$"; +const ALGO_BASED_REGEX: &str = r"^\s*\\?(?P(?:[A-Z0-9]+|BLAKE2b))(?:-(?P\d+))?\s?\((?P(?-u:.*))\)\s*=\s*(?P[A-Za-z0-9+/]+={0,2})$"; const DOUBLE_SPACE_REGEX: &str = r"^(?P[a-fA-F0-9]+)\s{2}(?P(?-u:.*))$"; // In this case, we ignore the * const SINGLE_SPACE_REGEX: &str = r"^(?P[a-fA-F0-9]+)\s(?P\*?(?-u:.*))$"; +lazy_static! { + static ref R_ALGO_BASED: Regex = Regex::new(ALGO_BASED_REGEX).unwrap(); + static ref R_DOUBLE_SPACE: Regex = Regex::new(DOUBLE_SPACE_REGEX).unwrap(); + static ref R_SINGLE_SPACE: Regex = Regex::new(SINGLE_SPACE_REGEX).unwrap(); +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +enum LineFormat { + AlgoBased, + SingleSpace, + DoubleSpace, +} + +impl LineFormat { + fn to_regex(self) -> &'static Regex { + match self { + LineFormat::AlgoBased => &R_ALGO_BASED, + LineFormat::SingleSpace => &R_SINGLE_SPACE, + LineFormat::DoubleSpace => &R_DOUBLE_SPACE, + } + } +} + +/// Hold the data extracted from a checksum line. +struct LineInfo { + algo_name: Option, + algo_bit_len: Option, + checksum: String, + filename: Vec, + + format: LineFormat, +} + +impl LineInfo { + /// Returns a `LineInfo` parsed from a checksum line. + /// The function will run 3 regexes against the line and select the first one that matches + /// to populate the fields of the struct. + /// However, there is a catch to handle regarding the handling of `cached_regex`. + /// In case of non-algo-based regex, if `cached_regex` is Some, it must take the priority + /// over the detected regex. Otherwise, we must set it the the detected regex. + /// This specific behavior is emphasized by the test + /// `test_hashsum::test_check_md5sum_only_one_space`. + fn parse(s: impl AsRef, cached_regex: &mut Option) -> Option { + let regexes: &[(&'static Regex, LineFormat)] = &[ + (&R_ALGO_BASED, LineFormat::AlgoBased), + (&R_DOUBLE_SPACE, LineFormat::DoubleSpace), + (&R_SINGLE_SPACE, LineFormat::SingleSpace), + ]; + + let line_bytes = os_str_as_bytes(s.as_ref()).expect("UTF-8 decoding failed"); + + for (regex, format) in regexes { + if !regex.is_match(line_bytes) { + continue; + } + + let mut r = *regex; + if *format != LineFormat::AlgoBased { + // The cached regex ensures that when processing non-algo based regexes, + // it cannot be changed (can't have single and double space regexes + // used in the same file). + if cached_regex.is_some() { + r = cached_regex.unwrap().to_regex(); + } else { + *cached_regex = Some(*format); + } + } + + if let Some(caps) = r.captures(line_bytes) { + // These unwraps are safe thanks to the regex + let match_to_string = |m: Match| String::from_utf8(m.as_bytes().into()).unwrap(); + + return Some(Self { + algo_name: caps.name("algo").map(match_to_string), + algo_bit_len: caps + .name("bits") + .map(|m| match_to_string(m).parse::().unwrap()), + checksum: caps.name("checksum").map(match_to_string).unwrap(), + filename: caps.name("filename").map(|m| m.as_bytes().into()).unwrap(), + format: *format, + }); + } + } + + None + } +} + fn get_filename_for_output(filename: &OsStr, input_is_stdin: bool) -> String { if input_is_stdin { "standard input" @@ -326,109 +526,90 @@ fn get_filename_for_output(filename: &OsStr, input_is_stdin: bool) -> String { .to_string() } -/// Determines the appropriate regular expression to use based on the provided lines. -fn determine_regex(lines: &[OsString]) -> Option<(Regex, bool)> { - let regexes = [ - (Regex::new(ALGO_BASED_REGEX).unwrap(), true), - (Regex::new(DOUBLE_SPACE_REGEX).unwrap(), false), - (Regex::new(SINGLE_SPACE_REGEX).unwrap(), false), - (Regex::new(ALGO_BASED_REGEX_BASE64).unwrap(), true), - ]; +/// Extract the expected digest from the checksum string +fn get_expected_digest_as_hex_string( + line_info: &LineInfo, + len_hint: Option, +) -> Option> { + let ck = &line_info.checksum; - for line in lines { - let line_bytes = os_str_as_bytes(line).expect("UTF-8 decoding failed"); - for (regex, is_algo_based) in ®exes { - if regex.is_match(line_bytes) { - return Some((regex.clone(), *is_algo_based)); - } - } + // TODO MSRV 1.82, replace `is_some_and` with `is_none_or` + // to improve readability. This closure returns True if a length hint provided + // and the argument isn't the same as the hint. + let against_hint = |len| len_hint.is_some_and(|l| l != len); + + if ck.len() % 2 != 0 { + // If the length of the digest is not a multiple of 2, then it + // must be improperly formatted (1 hex digit is 2 characters) + return None; } - None -} + // If the digest can be decoded as hexadecimal AND it length match the + // one expected (in case it's given), just go with it. + if ck.as_bytes().iter().all(u8::is_ascii_hexdigit) && !against_hint(ck.len()) { + return Some(Cow::Borrowed(ck)); + } -// Converts bytes to a hexadecimal string -fn bytes_to_hex(bytes: &[u8]) -> String { - use std::fmt::Write; - bytes - .iter() - .fold(String::with_capacity(bytes.len() * 2), |mut hex, byte| { - write!(hex, "{byte:02x}").unwrap(); - hex + // If hexadecimal digest fails for any reason, interpret the digest as base 64. + BASE64 + .decode(ck.as_bytes()) // Decode the string as encoded base64 + .map(hex::encode) // Encode it back as hexadecimal + .map(Cow::::Owned) + .ok() + .and_then(|s| { + // Check the digest length + if !against_hint(s.len()) { + Some(s) + } else { + None + } }) } -fn get_expected_checksum( - filename: &[u8], - caps: &Captures, - chosen_regex: &Regex, -) -> UResult { - if chosen_regex.as_str() == ALGO_BASED_REGEX_BASE64 { - // Unwrap is safe, ensured by regex - let ck = caps.name("checksum").unwrap().as_bytes(); - match BASE64.decode(ck) { - Ok(decoded_bytes) => { - match std::str::from_utf8(&decoded_bytes) { - Ok(decoded_str) => Ok(decoded_str.to_string()), - Err(_) => Ok(bytes_to_hex(&decoded_bytes)), // Handle as raw bytes if not valid UTF-8 - } - } - Err(_) => Err(Box::new( - ChecksumError::NoProperlyFormattedChecksumLinesFound { - filename: String::from_utf8_lossy(filename).to_string(), - }, - )), - } - } else { - // Unwraps are safe, ensured by regex. - Ok(str::from_utf8(caps.name("checksum").unwrap().as_bytes()) - .unwrap() - .to_string()) - } -} - /// Returns a reader that reads from the specified file, or from stdin if `filename_to_check` is "-". fn get_file_to_check( filename: &OsStr, - ignore_missing: bool, - res: &mut ChecksumResult, -) -> Option> { + opts: ChecksumOptions, +) -> Result, LineCheckError> { let filename_bytes = os_str_as_bytes(filename).expect("UTF-8 error"); let filename_lossy = String::from_utf8_lossy(filename_bytes); if filename == "-" { - Some(Box::new(stdin())) // Use stdin if "-" is specified in the checksum file + Ok(Box::new(stdin())) // Use stdin if "-" is specified in the checksum file } else { - let mut failed_open = || { + let failed_open = || { print_file_report( std::io::stdout(), filename_bytes, FileChecksumResult::CantOpen, "", + opts, ); - res.failed_open_file += 1; }; match File::open(filename) { Ok(f) => { - if f.metadata().ok()?.is_dir() { + if f.metadata() + .map_err(|_| LineCheckError::CantOpenFile)? + .is_dir() + { show!(USimpleError::new( 1, format!("{filename_lossy}: Is a directory") )); // also regarded as a failed open failed_open(); - None + Err(LineCheckError::FileIsDirectory) } else { - Some(Box::new(f)) + Ok(Box::new(f)) } } Err(err) => { - if !ignore_missing { + if !opts.ignore_missing { // yes, we have both stderr and stdout here show!(err.map_err_context(|| filename_lossy.to_string())); failed_open(); } // we could not open the file but we want to continue - None + Err(LineCheckError::FileNotFound) } } } @@ -456,254 +637,318 @@ fn get_input_file(filename: &OsStr) -> UResult> { } } -/// Extracts the algorithm name and length from the regex captures if the algo-based format is matched. +/// Gets the algorithm name and length from the `LineInfo` if the algo-based format is matched. fn identify_algo_name_and_length( - caps: &Captures, + line_info: &LineInfo, algo_name_input: Option<&str>, - res: &mut ChecksumResult, - properly_formatted: &mut bool, ) -> Option<(String, Option)> { - // When the algo-based format is matched, extract details from regex captures - let algorithm = caps - .name("algo") - .map_or(String::new(), |m| { - String::from_utf8(m.as_bytes().into()).unwrap() - }) + let algorithm = line_info + .algo_name + .clone() + .unwrap_or_default() .to_lowercase(); // check if we are called with XXXsum (example: md5sum) but we detected a different algo parsing the file // (for example SHA1 (f) = d...) // Also handle the case cksum -s sm3 but the file contains other formats if algo_name_input.is_some() && algo_name_input != Some(&algorithm) { - res.bad_format += 1; - *properly_formatted = false; return None; } if !SUPPORTED_ALGORITHMS.contains(&algorithm.as_str()) { // Not supported algo, leave early - *properly_formatted = false; return None; } - let bits = caps.name("bits").map_or(Some(None), |m| { - let bits_value = String::from_utf8(m.as_bytes().into()) - .unwrap() - .parse::() - .unwrap(); - if bits_value % 8 == 0 { - Some(Some(bits_value / 8)) - } else { - *properly_formatted = false; - None // Return None to signal a divisibility issue + let bytes = if let Some(bitlen) = line_info.algo_bit_len { + if bitlen % 8 != 0 { + // The given length is wrong + return None; } - })?; + Some(bitlen / 8) + } else if algorithm == ALGORITHM_OPTIONS_BLAKE2B { + // Default length with BLAKE2b, + Some(64) + } else { + None + }; - Some((algorithm, bits)) + Some((algorithm, bytes)) +} + +/// Given a filename and an algorithm, compute the digest and compare it with +/// the expected one. +fn compute_and_check_digest_from_file( + filename: &[u8], + expected_checksum: &str, + mut algo: HashAlgorithm, + opts: ChecksumOptions, +) -> Result<(), LineCheckError> { + let (filename_to_check_unescaped, prefix) = unescape_filename(filename); + let real_filename_to_check = os_str_from_bytes(&filename_to_check_unescaped)?; + + // Open the input file + let file_to_check = get_file_to_check(&real_filename_to_check, opts)?; + let mut file_reader = BufReader::new(file_to_check); + + // Read the file and calculate the checksum + let create_fn = &mut algo.create_fn; + let mut digest = create_fn(); + let (calculated_checksum, _) = + digest_reader(&mut digest, &mut file_reader, opts.binary, algo.bits).unwrap(); + + // Do the checksum validation + let checksum_correct = expected_checksum == calculated_checksum; + print_file_report( + std::io::stdout(), + filename, + FileChecksumResult::from_bool(checksum_correct), + prefix, + opts, + ); + + if checksum_correct { + Ok(()) + } else { + Err(LineCheckError::DigestMismatch) + } +} + +/// Check a digest checksum with non-algo based pre-treatment. +fn process_algo_based_line( + line_info: &LineInfo, + cli_algo_name: Option<&str>, + opts: ChecksumOptions, +) -> Result<(), LineCheckError> { + let filename_to_check = line_info.filename.as_slice(); + + let (algo_name, algo_byte_len) = identify_algo_name_and_length(line_info, cli_algo_name) + .ok_or(LineCheckError::ImproperlyFormatted)?; + + // If the digest bitlen is known, we can check the format of the expected + // checksum with it. + let digest_char_length_hint = match (algo_name.as_str(), algo_byte_len) { + (ALGORITHM_OPTIONS_BLAKE2B, Some(bytelen)) => Some(bytelen * 2), + _ => None, + }; + + let expected_checksum = get_expected_digest_as_hex_string(line_info, digest_char_length_hint) + .ok_or(LineCheckError::ImproperlyFormatted)?; + + let algo = detect_algo(&algo_name, algo_byte_len)?; + + compute_and_check_digest_from_file(filename_to_check, &expected_checksum, algo, opts) +} + +/// Check a digest checksum with non-algo based pre-treatment. +fn process_non_algo_based_line( + line_number: usize, + line_info: &LineInfo, + cli_algo_name: &str, + cli_algo_length: Option, + opts: ChecksumOptions, +) -> Result<(), LineCheckError> { + let mut filename_to_check = line_info.filename.as_slice(); + if filename_to_check.starts_with(b"*") + && line_number == 0 + && line_info.format == LineFormat::SingleSpace + { + // Remove the leading asterisk if present - only for the first line + filename_to_check = &filename_to_check[1..]; + } + let expected_checksum = get_expected_digest_as_hex_string(line_info, None) + .ok_or(LineCheckError::ImproperlyFormatted)?; + + // When a specific algorithm name is input, use it and use the provided bits + // except when dealing with blake2b, where we will detect the length + let (algo_name, algo_byte_len) = if cli_algo_name == ALGORITHM_OPTIONS_BLAKE2B { + // division by 2 converts the length of the Blake2b checksum from hexadecimal + // characters to bytes, as each byte is represented by two hexadecimal characters. + let length = Some(expected_checksum.len() / 2); + (ALGORITHM_OPTIONS_BLAKE2B.to_string(), length) + } else { + (cli_algo_name.to_lowercase(), cli_algo_length) + }; + + let algo = detect_algo(&algo_name, algo_byte_len)?; + + compute_and_check_digest_from_file(filename_to_check, &expected_checksum, algo, opts) +} + +/// Parses a checksum line, detect the algorithm to use, read the file and produce +/// its digest, and compare it to the expected value. +/// +/// Returns `Ok(bool)` if the comparison happened, bool indicates if the digest +/// matched the expected. +/// If the comparison didn't happen, return a `LineChecksumError`. +fn process_checksum_line( + filename_input: &OsStr, + line: &OsStr, + i: usize, + cli_algo_name: Option<&str>, + cli_algo_length: Option, + opts: ChecksumOptions, + cached_regex: &mut Option, +) -> Result<(), LineCheckError> { + let line_bytes = os_str_as_bytes(line)?; + + // Early return on empty or commented lines. + if line.is_empty() || line_bytes.starts_with(b"#") { + return Err(LineCheckError::Skipped); + } + + // Use `LineInfo` to extract the data of a line. + // Then, depending on its format, apply a different pre-treatment. + if let Some(line_info) = LineInfo::parse(line, cached_regex) { + if line_info.format == LineFormat::AlgoBased { + process_algo_based_line(&line_info, cli_algo_name, opts) + } else if let Some(cli_algo) = cli_algo_name { + // If we match a non-algo based regex, we expect a cli argument + // to give us the algorithm to use + process_non_algo_based_line(i, &line_info, cli_algo, cli_algo_length, opts) + } else { + // We have no clue of what algorithm to use + return Err(LineCheckError::ImproperlyFormatted); + } + } else { + if opts.warn { + let algo = if let Some(algo_name_input) = cli_algo_name { + algo_name_input.to_uppercase() + } else { + "Unknown algorithm".to_string() + }; + eprintln!( + "{}: {}: {}: improperly formatted {} checksum line", + util_name(), + &filename_input.maybe_quote(), + i + 1, + algo + ); + } + + Err(LineCheckError::ImproperlyFormatted) + } +} + +fn process_checksum_file( + filename_input: &OsStr, + cli_algo_name: Option<&str>, + cli_algo_length: Option, + opts: ChecksumOptions, +) -> Result<(), FileCheckError> { + let mut res = ChecksumResult::default(); + + let input_is_stdin = filename_input == OsStr::new("-"); + + let file: Box = if input_is_stdin { + // Use stdin if "-" is specified + Box::new(stdin()) + } else { + match get_input_file(filename_input) { + Ok(f) => f, + Err(e) => { + // Could not read the file, show the error and continue to the next file + show_error!("{e}"); + set_exit_code(1); + return Err(FileCheckError::CantOpenChecksumFile); + } + } + }; + + let reader = BufReader::new(file); + let lines = read_os_string_lines(reader).collect::>(); + + // cached_regex is used to ensure that several non algo-based checksum line + // will use the same regex. + let mut cached_regex = None; + + for (i, line) in lines.iter().enumerate() { + let line_result = process_checksum_line( + filename_input, + line, + i, + cli_algo_name, + cli_algo_length, + opts, + &mut cached_regex, + ); + + // Match a first time to elude critical UErrors, and increment the total + // in all cases except on skipped. + use LineCheckError::*; + match line_result { + Err(UError(e)) => return Err(e.into()), + Err(Skipped) => (), + _ => res.total += 1, + } + + // Match a second time to update the right field of `res`. + match line_result { + Ok(()) => res.correct += 1, + Err(DigestMismatch) => res.failed_cksum += 1, + Err(ImproperlyFormatted) => res.bad_format += 1, + Err(CantOpenFile | FileIsDirectory) => res.failed_open_file += 1, + Err(FileNotFound) if !opts.ignore_missing => res.failed_open_file += 1, + _ => continue, + }; + } + + // not a single line correctly formatted found + // return an error + if res.total_properly_formatted() == 0 { + if !opts.status { + log_no_properly_formatted(get_filename_for_output(filename_input, input_is_stdin)); + } + set_exit_code(1); + return Err(FileCheckError::ImproperlyFormatted); + } + + // if any incorrectly formatted line, show it + cksum_output(&res, opts.status); + + if opts.ignore_missing && res.correct == 0 { + // we have only bad format + // and we had ignore-missing + eprintln!( + "{}: {}: no file was verified", + util_name(), + filename_input.maybe_quote(), + ); + set_exit_code(1); + } + + // strict means that we should have an exit code. + if opts.strict && res.bad_format > 0 { + set_exit_code(1); + } + + // if we have any failed checksum verification, we set an exit code + // except if we have ignore_missing + if (res.failed_cksum > 0 || res.failed_open_file > 0) && !opts.ignore_missing { + set_exit_code(1); + } + + Ok(()) } /*** * Do the checksum validation (can be strict or not) */ -#[allow(clippy::too_many_arguments)] pub fn perform_checksum_validation<'a, I>( files: I, - strict: bool, - status: bool, - warn: bool, - binary: bool, - ignore_missing: bool, - quiet: bool, algo_name_input: Option<&str>, length_input: Option, + opts: ChecksumOptions, ) -> UResult<()> where I: Iterator, { // if cksum has several input files, it will print the result for each file for filename_input in files { - let mut correct_format = 0; - let mut properly_formatted = false; - let mut res = ChecksumResult::default(); - let input_is_stdin = filename_input == OsStr::new("-"); - - let file: Box = if input_is_stdin { - // Use stdin if "-" is specified - Box::new(stdin()) - } else { - match get_input_file(filename_input) { - Ok(f) => f, - Err(e) => { - // Could not read the file, show the error and continue to the next file - show_error!("{e}"); - set_exit_code(1); - continue; - } - } - }; - - let reader = BufReader::new(file); - let lines = read_os_string_lines(reader).collect::>(); - - let Some((chosen_regex, is_algo_based_format)) = determine_regex(&lines) else { - let e = ChecksumError::NoProperlyFormattedChecksumLinesFound { - filename: get_filename_for_output(filename_input, input_is_stdin), - }; - show_error!("{e}"); - set_exit_code(1); - continue; - }; - - for (i, line) in lines.iter().enumerate() { - let line_bytes = os_str_as_bytes(line)?; - if let Some(caps) = chosen_regex.captures(line_bytes) { - properly_formatted = true; - - let mut filename_to_check = caps.name("filename").unwrap().as_bytes(); - - if filename_to_check.starts_with(b"*") - && i == 0 - && chosen_regex.as_str() == SINGLE_SPACE_REGEX - { - // Remove the leading asterisk if present - only for the first line - filename_to_check = &filename_to_check[1..]; - } - - let expected_checksum = - get_expected_checksum(filename_to_check, &caps, &chosen_regex)?; - - // If the algo_name is provided, we use it, otherwise we try to detect it - let (algo_name, length) = if is_algo_based_format { - identify_algo_name_and_length( - &caps, - algo_name_input, - &mut res, - &mut properly_formatted, - ) - .unwrap_or((String::new(), None)) - } else if let Some(a) = algo_name_input { - // When a specific algorithm name is input, use it and use the provided bits - // except when dealing with blake2b, where we will detect the length - if algo_name_input == Some(ALGORITHM_OPTIONS_BLAKE2B) { - // division by 2 converts the length of the Blake2b checksum from hexadecimal - // characters to bytes, as each byte is represented by two hexadecimal characters. - let length = Some(expected_checksum.len() / 2); - (ALGORITHM_OPTIONS_BLAKE2B.to_string(), length) - } else { - (a.to_lowercase(), length_input) - } - } else { - // Default case if no algorithm is specified and non-algo based format is matched - (String::new(), None) - }; - - if algo_name.is_empty() { - // we haven't been able to detect the algo name. No point to continue - properly_formatted = false; - continue; - } - let mut algo = detect_algo(&algo_name, length)?; - - let (filename_to_check_unescaped, prefix) = unescape_filename(filename_to_check); - - let real_filename_to_check = os_str_from_bytes(&filename_to_check_unescaped)?; - - // manage the input file - let file_to_check = - match get_file_to_check(&real_filename_to_check, ignore_missing, &mut res) { - Some(file) => file, - None => continue, - }; - let mut file_reader = BufReader::new(file_to_check); - // Read the file and calculate the checksum - let create_fn = &mut algo.create_fn; - let mut digest = create_fn(); - let (calculated_checksum, _) = - digest_reader(&mut digest, &mut file_reader, binary, algo.bits).unwrap(); - - // Do the checksum validation - if expected_checksum == calculated_checksum { - if !quiet && !status { - print_file_report( - std::io::stdout(), - filename_to_check, - FileChecksumResult::Ok, - prefix, - ); - } - correct_format += 1; - } else { - if !status { - print_file_report( - std::io::stdout(), - filename_to_check, - FileChecksumResult::Failed, - prefix, - ); - } - res.failed_cksum += 1; - } - } else { - if line.is_empty() || line_bytes.starts_with(b"#") { - // Don't show any warning for empty or commented lines. - continue; - } - if warn { - let algo = if let Some(algo_name_input) = algo_name_input { - algo_name_input.to_uppercase() - } else { - "Unknown algorithm".to_string() - }; - eprintln!( - "{}: {}: {}: improperly formatted {} checksum line", - util_name(), - &filename_input.maybe_quote(), - i + 1, - algo - ); - } - - res.bad_format += 1; - } - } - - // not a single line correctly formatted found - // return an error - if !properly_formatted { - if !status { - return Err(ChecksumError::NoProperlyFormattedChecksumLinesFound { - filename: get_filename_for_output(filename_input, input_is_stdin), - } - .into()); - } - set_exit_code(1); - - return Ok(()); - } - - // if any incorrectly formatted line, show it - cksum_output(&res, status); - - if ignore_missing && correct_format == 0 { - // we have only bad format - // and we had ignore-missing - eprintln!( - "{}: {}: no file was verified", - util_name(), - filename_input.maybe_quote(), - ); - set_exit_code(1); - } - - // strict means that we should have an exit code. - if strict && res.bad_format > 0 { - set_exit_code(1); - } - - // if we have any failed checksum verification, we set an exit code - // except if we have ignore_missing - if (res.failed_cksum > 0 || res.failed_open_file > 0) && !ignore_missing { - set_exit_code(1); + use FileCheckError::*; + match process_checksum_file(filename_input, algo_name_input, length_input, opts) { + Err(UError(e)) => return Err(e), + Err(CantOpenChecksumFile | ImproperlyFormatted) | Ok(_) => continue, } } @@ -812,6 +1057,7 @@ pub fn escape_filename(filename: &Path) -> (String, &'static str) { #[cfg(test)] mod tests { use super::*; + use std::ffi::OsString; #[test] fn test_unescape_filename() { @@ -942,7 +1188,7 @@ mod tests { ]; for (input, expected) in test_cases { - let captures = algo_based_regex.captures(*input); + let captures = algo_based_regex.captures(input); match expected { Some((algo, bits, filename, checksum)) => { assert!(captures.is_some()); @@ -1045,79 +1291,71 @@ mod tests { } #[test] - fn test_determine_regex() { + fn test_line_info() { + let mut cached_regex = None; + // Test algo-based regex - let lines_algo_based = ["MD5 (example.txt) = d41d8cd98f00b204e9800998ecf8427e"] - .iter() - .map(|s| OsString::from(s.to_string())) - .collect::>(); - let (regex, algo_based) = determine_regex(&lines_algo_based).unwrap(); - assert!(algo_based); - assert!(regex.is_match(os_str_as_bytes(&lines_algo_based[0]).unwrap())); + let line_algo_based = + OsString::from("MD5 (example.txt) = d41d8cd98f00b204e9800998ecf8427e"); + let line_info = LineInfo::parse(&line_algo_based, &mut cached_regex).unwrap(); + assert_eq!(line_info.algo_name.as_deref(), Some("MD5")); + assert!(line_info.algo_bit_len.is_none()); + assert_eq!(line_info.filename, b"example.txt"); + assert_eq!(line_info.checksum, "d41d8cd98f00b204e9800998ecf8427e"); + assert_eq!(line_info.format, LineFormat::AlgoBased); + assert!(cached_regex.is_none()); // Test double-space regex - let lines_double_space = ["d41d8cd98f00b204e9800998ecf8427e example.txt"] - .iter() - .map(|s| OsString::from(s.to_string())) - .collect::>(); - let (regex, algo_based) = determine_regex(&lines_double_space).unwrap(); - assert!(!algo_based); - assert!(regex.is_match(os_str_as_bytes(&lines_double_space[0]).unwrap())); + let line_double_space = OsString::from("d41d8cd98f00b204e9800998ecf8427e example.txt"); + let line_info = LineInfo::parse(&line_double_space, &mut cached_regex).unwrap(); + assert!(line_info.algo_name.is_none()); + assert!(line_info.algo_bit_len.is_none()); + assert_eq!(line_info.filename, b"example.txt"); + assert_eq!(line_info.checksum, "d41d8cd98f00b204e9800998ecf8427e"); + assert_eq!(line_info.format, LineFormat::DoubleSpace); + assert!(cached_regex.is_some()); + + cached_regex = None; // Test single-space regex - let lines_single_space = ["d41d8cd98f00b204e9800998ecf8427e example.txt"] - .iter() - .map(|s| OsString::from(s.to_string())) - .collect::>(); - let (regex, algo_based) = determine_regex(&lines_single_space).unwrap(); - assert!(!algo_based); - assert!(regex.is_match(os_str_as_bytes(&lines_single_space[0]).unwrap())); + let line_single_space = OsString::from("d41d8cd98f00b204e9800998ecf8427e example.txt"); + let line_info = LineInfo::parse(&line_single_space, &mut cached_regex).unwrap(); + assert!(line_info.algo_name.is_none()); + assert!(line_info.algo_bit_len.is_none()); + assert_eq!(line_info.filename, b"example.txt"); + assert_eq!(line_info.checksum, "d41d8cd98f00b204e9800998ecf8427e"); + assert_eq!(line_info.format, LineFormat::SingleSpace); + assert!(cached_regex.is_some()); - // Test double-space regex start with invalid - let lines_double_space = ["ERR", "d41d8cd98f00b204e9800998ecf8427e example.txt"] - .iter() - .map(|s| OsString::from(s.to_string())) - .collect::>(); - let (regex, algo_based) = determine_regex(&lines_double_space).unwrap(); - assert!(!algo_based); - assert!(!regex.is_match(os_str_as_bytes(&lines_double_space[0]).unwrap())); - assert!(regex.is_match(os_str_as_bytes(&lines_double_space[1]).unwrap())); + cached_regex = None; // Test invalid checksum line - let lines_invalid = ["invalid checksum line"] - .iter() - .map(|s| OsString::from(s.to_string())) - .collect::>(); - assert!(determine_regex(&lines_invalid).is_none()); + let line_invalid = OsString::from("invalid checksum line"); + assert!(LineInfo::parse(&line_invalid, &mut cached_regex).is_none()); + assert!(cached_regex.is_none()); // Test leading space before checksum line - let lines_algo_based_leading_space = - vec![" MD5 (example.txt) = d41d8cd98f00b204e9800998ecf8427e"] - .iter() - .map(|s| OsString::from(s.to_string())) - .collect::>(); - let res = determine_regex(&lines_algo_based_leading_space); - assert!(res.is_some()); - assert_eq!(res.unwrap().0.as_str(), ALGO_BASED_REGEX); + let line_algo_based_leading_space = + OsString::from(" MD5 (example.txt) = d41d8cd98f00b204e9800998ecf8427e"); + let line_info = LineInfo::parse(&line_algo_based_leading_space, &mut cached_regex).unwrap(); + assert_eq!(line_info.format, LineFormat::AlgoBased); + assert!(cached_regex.is_none()); // Test trailing space after checksum line (should fail) - let lines_algo_based_leading_space = - vec!["MD5 (example.txt) = d41d8cd98f00b204e9800998ecf8427e "] - .iter() - .map(|s| OsString::from(s.to_string())) - .collect::>(); - let res = determine_regex(&lines_algo_based_leading_space); + let line_algo_based_leading_space = + OsString::from("MD5 (example.txt) = d41d8cd98f00b204e9800998ecf8427e "); + let res = LineInfo::parse(&line_algo_based_leading_space, &mut cached_regex); assert!(res.is_none()); + assert!(cached_regex.is_none()); } #[test] - fn test_get_expected_checksum() { - let re = Regex::new(ALGO_BASED_REGEX_BASE64).unwrap(); - let caps = re - .captures(b"SHA256 (empty) = 47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=") - .unwrap(); + fn test_get_expected_digest() { + let line = OsString::from("SHA256 (empty) = 47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU="); + let mut cached_regex = None; + let line_info = LineInfo::parse(&line, &mut cached_regex).unwrap(); - let result = get_expected_checksum(b"filename", &caps, &re); + let result = get_expected_digest_as_hex_string(&line_info, None); assert_eq!( result.unwrap(), @@ -1127,18 +1365,20 @@ mod tests { #[test] fn test_get_expected_checksum_invalid() { - let re = Regex::new(ALGO_BASED_REGEX_BASE64).unwrap(); - let caps = re - .captures(b"SHA256 (empty) = 47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU") - .unwrap(); + // The line misses a '=' at the end to be valid base64 + let line = OsString::from("SHA256 (empty) = 47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU"); + let mut cached_regex = None; + let line_info = LineInfo::parse(&line, &mut cached_regex).unwrap(); - let result = get_expected_checksum(b"filename", &caps, &re); + let result = get_expected_digest_as_hex_string(&line_info, None); - assert!(result.is_err()); + assert!(result.is_none()); } #[test] fn test_print_file_report() { + let opts = ChecksumOptions::default(); + let cases: &[(&[u8], FileChecksumResult, &str, &[u8])] = &[ (b"filename", FileChecksumResult::Ok, "", b"filename: OK\n"), ( @@ -1169,7 +1409,7 @@ mod tests { for (filename, result, prefix, expected) in cases { let mut buffer: Vec = vec![]; - print_file_report(&mut buffer, filename, *result, prefix); + print_file_report(&mut buffer, filename, *result, prefix, opts); assert_eq!(&buffer, expected) } } diff --git a/src/uucore/src/lib/features/colors.rs b/src/uucore/src/lib/features/colors.rs index f05739431..885ae2fe9 100644 --- a/src/uucore/src/lib/features/colors.rs +++ b/src/uucore/src/lib/features/colors.rs @@ -13,6 +13,7 @@ /// restrict following config to systems with matching environment variables. pub static TERMS: &[&str] = &[ "Eterm", + "alacritty*", "ansi", "*color*", "con[0-9]*x[0-9]*", @@ -21,6 +22,7 @@ pub static TERMS: &[&str] = &[ "cygwin", "*direct*", "dtterm", + "foot", "gnome", "hurd", "jfbterm", diff --git a/src/uucore/src/lib/features/entries.rs b/src/uucore/src/lib/features/entries.rs index d1c9f9c04..f3d1232eb 100644 --- a/src/uucore/src/lib/features/entries.rs +++ b/src/uucore/src/lib/features/entries.rs @@ -83,13 +83,14 @@ pub fn get_groups() -> IOResult> { if res == -1 { let err = IOError::last_os_error(); if err.raw_os_error() == Some(libc::EINVAL) { - // Number of groups changed, retry + // Number of groups has increased, retry continue; } else { return Err(err); } } else { - groups.truncate(ngroups.try_into().unwrap()); + // Number of groups may have decreased + groups.truncate(res.try_into().unwrap()); return Ok(groups); } } diff --git a/src/uucore/src/lib/features/format/argument.rs b/src/uucore/src/lib/features/format/argument.rs index 758510498..5cdd03421 100644 --- a/src/uucore/src/lib/features/format/argument.rs +++ b/src/uucore/src/lib/features/format/argument.rs @@ -112,7 +112,8 @@ fn extract_value(p: Result>, input: &str) -> T Default::default() } ParseError::PartialMatch(v, rest) => { - if input.starts_with('\'') { + let bytes = input.as_encoded_bytes(); + if !bytes.is_empty() && bytes[0] == b'\'' { show_warning!( "{}: character(s) following character constant have been ignored", &rest, diff --git a/src/uucore/src/lib/features/format/mod.rs b/src/uucore/src/lib/features/format/mod.rs index 6d7a2ee30..25d128ed8 100644 --- a/src/uucore/src/lib/features/format/mod.rs +++ b/src/uucore/src/lib/features/format/mod.rs @@ -38,7 +38,7 @@ pub mod num_parser; mod spec; pub use argument::*; -use spec::Spec; +pub use spec::Spec; use std::{ error::Error, fmt::Display, @@ -48,7 +48,7 @@ use std::{ use crate::error::UError; -use self::{ +pub use self::{ escape::{parse_escape_code, EscapedChar}, num_format::Formatter, }; diff --git a/src/uucore/src/lib/features/format/spec.rs b/src/uucore/src/lib/features/format/spec.rs index 581e1fa06..81dbc1ebc 100644 --- a/src/uucore/src/lib/features/format/spec.rs +++ b/src/uucore/src/lib/features/format/spec.rs @@ -353,20 +353,20 @@ impl Spec { writer.write_all(&parsed).map_err(FormatError::IoError) } Self::QuotedString => { - let s = args.get_str(); - writer - .write_all( - escape_name( - s.as_ref(), - &QuotingStyle::Shell { - escape: true, - always_quote: false, - show_control: false, - }, - ) - .as_bytes(), - ) - .map_err(FormatError::IoError) + let s = escape_name( + args.get_str().as_ref(), + &QuotingStyle::Shell { + escape: true, + always_quote: false, + show_control: false, + }, + ); + #[cfg(unix)] + let bytes = std::os::unix::ffi::OsStringExt::into_vec(s); + #[cfg(not(unix))] + let bytes = s.to_string_lossy().as_bytes().to_owned(); + + writer.write_all(&bytes).map_err(FormatError::IoError) } Self::SignedInt { width, diff --git a/src/uucore/src/lib/features/fs.rs b/src/uucore/src/lib/features/fs.rs index e0c8ea79d..e2958232f 100644 --- a/src/uucore/src/lib/features/fs.rs +++ b/src/uucore/src/lib/features/fs.rs @@ -20,6 +20,7 @@ use std::ffi::{OsStr, OsString}; use std::fs; use std::fs::read_dir; use std::hash::Hash; +use std::io::Stdin; use std::io::{Error, ErrorKind, Result as IOResult}; #[cfg(unix)] use std::os::unix::{fs::MetadataExt, io::AsRawFd}; @@ -709,7 +710,7 @@ pub fn path_ends_with_terminator(path: &Path) -> bool { path.as_os_str() .as_bytes() .last() - .map_or(false, |&byte| byte == b'/' || byte == b'\\') + .is_some_and(|&byte| byte == b'/' || byte == b'\\') } #[cfg(windows)] @@ -721,6 +722,34 @@ pub fn path_ends_with_terminator(path: &Path) -> bool { .map_or(false, |wide| wide == b'/'.into() || wide == b'\\'.into()) } +/// Checks if the standard input (stdin) is a directory. +/// +/// # Arguments +/// +/// * `stdin` - A reference to the standard input handle. +/// +/// # Returns +/// +/// * `bool` - Returns `true` if stdin is a directory, `false` otherwise. +pub fn is_stdin_directory(stdin: &Stdin) -> bool { + #[cfg(unix)] + { + use nix::sys::stat::fstat; + let mode = fstat(stdin.as_raw_fd()).unwrap().st_mode as mode_t; + has!(mode, S_IFDIR) + } + + #[cfg(windows)] + { + use std::os::windows::io::AsRawHandle; + let handle = stdin.as_raw_handle(); + if let Ok(metadata) = fs::metadata(format!("{}", handle as usize)) { + return metadata.is_dir(); + } + false + } +} + pub mod sane_blksize { #[cfg(not(target_os = "windows"))] diff --git a/src/uucore/src/lib/features/perms.rs b/src/uucore/src/lib/features/perms.rs index ebb97042e..3623e9e61 100644 --- a/src/uucore/src/lib/features/perms.rs +++ b/src/uucore/src/lib/features/perms.rs @@ -23,7 +23,7 @@ use std::fs::Metadata; use std::os::unix::fs::MetadataExt; use std::os::unix::ffi::OsStrExt; -use std::path::{Path, MAIN_SEPARATOR_STR}; +use std::path::{Path, MAIN_SEPARATOR}; /// The various level of verbosity #[derive(PartialEq, Eq, Clone, Debug)] @@ -214,23 +214,13 @@ fn is_root(path: &Path, would_traverse_symlink: bool) -> bool { // We cannot check path.is_dir() here, as this would resolve symlinks, // which we need to avoid here. // All directory-ish paths match "*/", except ".", "..", "*/.", and "*/..". - let looks_like_dir = match path.as_os_str().to_str() { - // If it contains special character, prefer to err on the side of safety, i.e. forbidding the chown operation: - None => false, - Some(".") | Some("..") => true, - Some(path_str) => { - (path_str.ends_with(MAIN_SEPARATOR_STR)) - || (path_str.ends_with(&format!("{MAIN_SEPARATOR_STR}."))) - || (path_str.ends_with(&format!("{MAIN_SEPARATOR_STR}.."))) - } - }; - // TODO: Once we reach MSRV 1.74.0, replace this abomination by something simpler, e.g. this: - // let path_bytes = path.as_os_str().as_encoded_bytes(); - // let looks_like_dir = path_bytes == [b'.'] - // || path_bytes == [b'.', b'.'] - // || path_bytes.ends_with(&[MAIN_SEPARATOR as u8]) - // || path_bytes.ends_with(&[MAIN_SEPARATOR as u8, b'.']) - // || path_bytes.ends_with(&[MAIN_SEPARATOR as u8, b'.', b'.']); + let path_bytes = path.as_os_str().as_encoded_bytes(); + let looks_like_dir = path_bytes == [b'.'] + || path_bytes == [b'.', b'.'] + || path_bytes.ends_with(&[MAIN_SEPARATOR as u8]) + || path_bytes.ends_with(&[MAIN_SEPARATOR as u8, b'.']) + || path_bytes.ends_with(&[MAIN_SEPARATOR as u8, b'.', b'.']); + if !looks_like_dir { return false; } diff --git a/src/uucore/src/lib/features/quoting_style.rs b/src/uucore/src/lib/features/quoting_style.rs index 1efa6f746..6d0265dc6 100644 --- a/src/uucore/src/lib/features/quoting_style.rs +++ b/src/uucore/src/lib/features/quoting_style.rs @@ -6,39 +6,43 @@ //! Set of functions for escaping names according to different quoting styles. use std::char::from_digit; -use std::ffi::OsStr; +use std::ffi::{OsStr, OsString}; use std::fmt; // These are characters with special meaning in the shell (e.g. bash). // The first const contains characters that only have a special meaning when they appear at the beginning of a name. -const SPECIAL_SHELL_CHARS_START: &[char] = &['~', '#']; +const SPECIAL_SHELL_CHARS_START: &[u8] = b"~#"; // PR#6559 : Remove `]{}` from special shell chars. const SPECIAL_SHELL_CHARS: &str = "`$&*()|[;\\'\"<>?! "; /// The quoting style to use when escaping a name. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum QuotingStyle { - /// Escape the name as a literal string. + /// Escape the name as a shell string. + /// Used in, e.g., `ls --quoting-style=shell`. Shell { /// Whether to escape characters in the name. + /// True in, e.g., `ls --quoting-style=shell-escape`. escape: bool, /// Whether to always quote the name. always_quote: bool, - /// Whether to show control characters. + /// Whether to show control and non-unicode characters, or replace them with `?`. show_control: bool, }, /// Escape the name as a C string. + /// Used in, e.g., `ls --quote-name`. C { /// The type of quotes to use. quotes: Quotes, }, - /// Escape the name as a literal string. + /// Do not escape the string. + /// Used in, e.g., `ls --literal`. Literal { - /// Whether to show control characters. + /// Whether to show control and non-unicode characters, or replace them with `?`. show_control: bool, }, } @@ -72,16 +76,24 @@ enum EscapeState { Octal(EscapeOctal), } +/// Bytes we need to present as escaped octal, in the form of `\nnn` per byte. +/// Only supports characters up to 2 bytes long in UTF-8. struct EscapeOctal { - c: char, + c: [u8; 2], state: EscapeOctalState, - idx: usize, + idx: u8, } enum EscapeOctalState { Done, - Backslash, - Value, + FirstBackslash, + FirstValue, + LastBackslash, + LastValue, +} + +fn byte_to_octal_digit(byte: u8, idx: u8) -> u8 { + (byte >> (idx * 3)) & 0o7 } impl Iterator for EscapeOctal { @@ -90,29 +102,57 @@ impl Iterator for EscapeOctal { fn next(&mut self) -> Option { match self.state { EscapeOctalState::Done => None, - EscapeOctalState::Backslash => { - self.state = EscapeOctalState::Value; + EscapeOctalState::FirstBackslash => { + self.state = EscapeOctalState::FirstValue; Some('\\') } - EscapeOctalState::Value => { - let octal_digit = ((self.c as u32) >> (self.idx * 3)) & 0o7; + EscapeOctalState::LastBackslash => { + self.state = EscapeOctalState::LastValue; + Some('\\') + } + EscapeOctalState::FirstValue => { + let octal_digit = byte_to_octal_digit(self.c[0], self.idx); + if self.idx == 0 { + self.state = EscapeOctalState::LastBackslash; + self.idx = 2; + } else { + self.idx -= 1; + } + Some(from_digit(octal_digit.into(), 8).unwrap()) + } + EscapeOctalState::LastValue => { + let octal_digit = byte_to_octal_digit(self.c[1], self.idx); if self.idx == 0 { self.state = EscapeOctalState::Done; } else { self.idx -= 1; } - Some(from_digit(octal_digit, 8).unwrap()) + Some(from_digit(octal_digit.into(), 8).unwrap()) } } } } impl EscapeOctal { - fn from(c: char) -> Self { + fn from_char(c: char) -> Self { + if c.len_utf8() == 1 { + return Self::from_byte(c as u8); + } + + let mut buf = [0; 2]; + let _s = c.encode_utf8(&mut buf); Self { - c, + c: buf, idx: 2, - state: EscapeOctalState::Backslash, + state: EscapeOctalState::FirstBackslash, + } + } + + fn from_byte(b: u8) -> Self { + Self { + c: [0, b], + idx: 2, + state: EscapeOctalState::LastBackslash, } } } @@ -124,6 +164,12 @@ impl EscapedChar { } } + fn new_octal(b: u8) -> Self { + Self { + state: EscapeState::Octal(EscapeOctal::from_byte(b)), + } + } + fn new_c(c: char, quotes: Quotes, dirname: bool) -> Self { use EscapeState::*; let init_state = match c { @@ -148,7 +194,7 @@ impl EscapedChar { _ => Char(' '), }, ':' if dirname => Backslash(':'), - _ if c.is_ascii_control() => Octal(EscapeOctal::from(c)), + _ if c.is_control() => Octal(EscapeOctal::from_char(c)), _ => Char(c), }; Self { state: init_state } @@ -165,11 +211,11 @@ impl EscapedChar { '\x0B' => Backslash('v'), '\x0C' => Backslash('f'), '\r' => Backslash('r'), - '\x00'..='\x1F' | '\x7F' => Octal(EscapeOctal::from(c)), '\'' => match quotes { Quotes::Single => Backslash('\''), _ => Char('\''), }, + _ if c.is_control() => Octal(EscapeOctal::from_char(c)), _ if SPECIAL_SHELL_CHARS.contains(c) => ForceQuote(c), _ => Char(c), }; @@ -205,102 +251,124 @@ impl Iterator for EscapedChar { } } -fn shell_without_escape(name: &str, quotes: Quotes, show_control_chars: bool) -> (String, bool) { +/// Check whether `bytes` starts with any byte in `pattern`. +fn bytes_start_with(bytes: &[u8], pattern: &[u8]) -> bool { + !bytes.is_empty() && pattern.contains(&bytes[0]) +} + +fn shell_without_escape(name: &[u8], quotes: Quotes, show_control_chars: bool) -> (Vec, bool) { let mut must_quote = false; - let mut escaped_str = String::with_capacity(name.len()); + let mut escaped_str = Vec::with_capacity(name.len()); + let mut utf8_buf = vec![0; 4]; - for c in name.chars() { - let escaped = { - let ec = EscapedChar::new_shell(c, false, quotes); - if show_control_chars { - ec - } else { - ec.hide_control() - } - }; + for s in name.utf8_chunks() { + for c in s.valid().chars() { + let escaped = { + let ec = EscapedChar::new_shell(c, false, quotes); + if show_control_chars { + ec + } else { + ec.hide_control() + } + }; - match escaped.state { - EscapeState::Backslash('\'') => escaped_str.push_str("'\\''"), - EscapeState::ForceQuote(x) => { - must_quote = true; - escaped_str.push(x); - } - _ => { - for char in escaped { - escaped_str.push(char); + match escaped.state { + EscapeState::Backslash('\'') => escaped_str.extend_from_slice(b"'\\''"), + EscapeState::ForceQuote(x) => { + must_quote = true; + escaped_str.extend_from_slice(x.encode_utf8(&mut utf8_buf).as_bytes()); + } + _ => { + for c in escaped { + escaped_str.extend_from_slice(c.encode_utf8(&mut utf8_buf).as_bytes()); + } } } } + + if show_control_chars { + escaped_str.extend_from_slice(s.invalid()); + } else { + escaped_str.resize(escaped_str.len() + s.invalid().len(), b'?'); + } } - must_quote = must_quote || name.starts_with(SPECIAL_SHELL_CHARS_START); + must_quote = must_quote || bytes_start_with(name, SPECIAL_SHELL_CHARS_START); (escaped_str, must_quote) } -fn shell_with_escape(name: &str, quotes: Quotes) -> (String, bool) { +fn shell_with_escape(name: &[u8], quotes: Quotes) -> (Vec, bool) { // We need to keep track of whether we are in a dollar expression // because e.g. \b\n is escaped as $'\b\n' and not like $'b'$'n' let mut in_dollar = false; let mut must_quote = false; let mut escaped_str = String::with_capacity(name.len()); - for c in name.chars() { - let escaped = EscapedChar::new_shell(c, true, quotes); - match escaped.state { - EscapeState::Char(x) => { - if in_dollar { - escaped_str.push_str("''"); + for s in name.utf8_chunks() { + for c in s.valid().chars() { + let escaped = EscapedChar::new_shell(c, true, quotes); + match escaped.state { + EscapeState::Char(x) => { + if in_dollar { + escaped_str.push_str("''"); + in_dollar = false; + } + escaped_str.push(x); + } + EscapeState::ForceQuote(x) => { + if in_dollar { + escaped_str.push_str("''"); + in_dollar = false; + } + must_quote = true; + escaped_str.push(x); + } + // Single quotes are not put in dollar expressions, but are escaped + // if the string also contains double quotes. In that case, they must + // be handled separately. + EscapeState::Backslash('\'') => { + must_quote = true; in_dollar = false; + escaped_str.push_str("'\\''"); } - escaped_str.push(x); - } - EscapeState::ForceQuote(x) => { - if in_dollar { - escaped_str.push_str("''"); - in_dollar = false; - } - must_quote = true; - escaped_str.push(x); - } - // Single quotes are not put in dollar expressions, but are escaped - // if the string also contains double quotes. In that case, they must - // be handled separately. - EscapeState::Backslash('\'') => { - must_quote = true; - in_dollar = false; - escaped_str.push_str("'\\''"); - } - _ => { - if !in_dollar { - escaped_str.push_str("'$'"); - in_dollar = true; - } - must_quote = true; - for char in escaped { - escaped_str.push(char); + _ => { + if !in_dollar { + escaped_str.push_str("'$'"); + in_dollar = true; + } + must_quote = true; + for char in escaped { + escaped_str.push(char); + } } } } + if !s.invalid().is_empty() { + if !in_dollar { + escaped_str.push_str("'$'"); + in_dollar = true; + } + must_quote = true; + let escaped_bytes: String = s + .invalid() + .iter() + .flat_map(|b| EscapedChar::new_octal(*b)) + .collect(); + escaped_str.push_str(&escaped_bytes); + } } - must_quote = must_quote || name.starts_with(SPECIAL_SHELL_CHARS_START); - (escaped_str, must_quote) + must_quote = must_quote || bytes_start_with(name, SPECIAL_SHELL_CHARS_START); + (escaped_str.into(), must_quote) } /// Return a set of characters that implies quoting of the word in /// shell-quoting mode. -fn shell_escaped_char_set(is_dirname: bool) -> &'static [char] { - const ESCAPED_CHARS: &[char] = &[ - // the ':' colon character only induce quoting in the - // context of ls displaying a directory name before listing its content. - // (e.g. with the recursive flag -R) - ':', - // Under this line are the control characters that should be - // quoted in shell mode in all cases. - '"', '`', '$', '\\', '^', '\n', '\t', '\r', '=', - ]; - +fn shell_escaped_char_set(is_dirname: bool) -> &'static [u8] { + const ESCAPED_CHARS: &[u8] = b":\"`$\\^\n\t\r="; + // the ':' colon character only induce quoting in the + // context of ls displaying a directory name before listing its content. + // (e.g. with the recursive flag -R) let start_index = if is_dirname { 0 } else { 1 }; - &ESCAPED_CHARS[start_index..] } @@ -308,41 +376,57 @@ fn shell_escaped_char_set(is_dirname: bool) -> &'static [char] { /// /// This inner function provides an additional flag `dirname` which /// is meant for ls' directory name display. -fn escape_name_inner(name: &OsStr, style: &QuotingStyle, dirname: bool) -> String { +fn escape_name_inner(name: &[u8], style: &QuotingStyle, dirname: bool) -> Vec { match style { QuotingStyle::Literal { show_control } => { if *show_control { - name.to_string_lossy().into_owned() + name.to_owned() } else { - name.to_string_lossy() - .chars() - .flat_map(|c| EscapedChar::new_literal(c).hide_control()) - .collect() + name.utf8_chunks() + .map(|s| { + let valid: String = s + .valid() + .chars() + .flat_map(|c| EscapedChar::new_literal(c).hide_control()) + .collect(); + let invalid = "?".repeat(s.invalid().len()); + valid + &invalid + }) + .collect::() + .into() } } QuotingStyle::C { quotes } => { let escaped_str: String = name - .to_string_lossy() - .chars() - .flat_map(|c| EscapedChar::new_c(c, *quotes, dirname)) - .collect(); + .utf8_chunks() + .flat_map(|s| { + let valid = s + .valid() + .chars() + .flat_map(|c| EscapedChar::new_c(c, *quotes, dirname)); + let invalid = s.invalid().iter().flat_map(|b| EscapedChar::new_octal(*b)); + valid.chain(invalid) + }) + .collect::(); match quotes { Quotes::Single => format!("'{escaped_str}'"), Quotes::Double => format!("\"{escaped_str}\""), Quotes::None => escaped_str, } + .into() } QuotingStyle::Shell { escape, always_quote, show_control, } => { - let name = name.to_string_lossy(); - - let (quotes, must_quote) = if name.contains(shell_escaped_char_set(dirname)) { + let (quotes, must_quote) = if name + .iter() + .any(|c| shell_escaped_char_set(dirname).contains(c)) + { (Quotes::Single, true) - } else if name.contains('\'') { + } else if name.contains(&b'\'') { (Quotes::Double, true) } else if *always_quote { (Quotes::Single, true) @@ -351,30 +435,43 @@ fn escape_name_inner(name: &OsStr, style: &QuotingStyle, dirname: bool) -> Strin }; let (escaped_str, contains_quote_chars) = if *escape { - shell_with_escape(&name, quotes) + shell_with_escape(name, quotes) } else { - shell_without_escape(&name, quotes, *show_control) + shell_without_escape(name, quotes, *show_control) }; - match (must_quote | contains_quote_chars, quotes) { - (true, Quotes::Single) => format!("'{escaped_str}'"), - (true, Quotes::Double) => format!("\"{escaped_str}\""), - _ => escaped_str, + if must_quote | contains_quote_chars && quotes != Quotes::None { + let mut quoted_str = Vec::::with_capacity(escaped_str.len() + 2); + let quote = if quotes == Quotes::Single { + b'\'' + } else { + b'"' + }; + quoted_str.push(quote); + quoted_str.extend(escaped_str); + quoted_str.push(quote); + quoted_str + } else { + escaped_str } } } } /// Escape a filename with respect to the given style. -pub fn escape_name(name: &OsStr, style: &QuotingStyle) -> String { - escape_name_inner(name, style, false) +pub fn escape_name(name: &OsStr, style: &QuotingStyle) -> OsString { + let name = crate::os_str_as_bytes_lossy(name); + crate::os_string_from_vec(escape_name_inner(&name, style, false)) + .expect("all byte sequences should be valid for platform, or already replaced in name") } /// Escape a directory name with respect to the given style. /// This is mainly meant to be used for ls' directory name printing and is not /// likely to be used elsewhere. -pub fn escape_dir_name(dir_name: &OsStr, style: &QuotingStyle) -> String { - escape_name_inner(dir_name, style, true) +pub fn escape_dir_name(dir_name: &OsStr, style: &QuotingStyle) -> OsString { + let name = crate::os_str_as_bytes_lossy(dir_name); + crate::os_string_from_vec(escape_name_inner(&name, style, true)) + .expect("all byte sequences should be valid for platform, or already replaced in name") } impl fmt::Display for QuotingStyle { @@ -415,7 +512,7 @@ impl fmt::Display for Quotes { #[cfg(test)] mod tests { - use crate::quoting_style::{escape_name, Quotes, QuotingStyle}; + use crate::quoting_style::{escape_name_inner, Quotes, QuotingStyle}; // spell-checker:ignore (tests/words) one\'two one'two @@ -465,14 +562,31 @@ mod tests { } } + fn check_names_inner(name: &[u8], map: &[(T, &str)]) -> Vec> { + map.iter() + .map(|(_, style)| escape_name_inner(name, &get_style(style), false)) + .collect() + } + fn check_names(name: &str, map: &[(&str, &str)]) { assert_eq!( map.iter() - .map(|(_, style)| escape_name(name.as_ref(), &get_style(style))) - .collect::>(), + .map(|(correct, _)| *correct) + .collect::>(), + check_names_inner(name.as_bytes(), map) + .iter() + .map(|bytes| std::str::from_utf8(bytes) + .expect("valid str goes in, valid str comes out")) + .collect::>() + ); + } + + fn check_names_raw(name: &[u8], map: &[(&[u8], &str)]) { + assert_eq!( map.iter() - .map(|(correct, _)| correct.to_string()) - .collect::>() + .map(|(correct, _)| *correct) + .collect::>(), + check_names_inner(name, map) ); } @@ -487,10 +601,10 @@ mod tests { ("\"one_two\"", "c"), ("one_two", "shell"), ("one_two", "shell-show"), - ("\'one_two\'", "shell-always"), - ("\'one_two\'", "shell-always-show"), + ("'one_two'", "shell-always"), + ("'one_two'", "shell-always-show"), ("one_two", "shell-escape"), - ("\'one_two\'", "shell-escape-always"), + ("'one_two'", "shell-escape-always"), ], ); } @@ -504,12 +618,12 @@ mod tests { ("one two", "literal-show"), ("one\\ two", "escape"), ("\"one two\"", "c"), - ("\'one two\'", "shell"), - ("\'one two\'", "shell-show"), - ("\'one two\'", "shell-always"), - ("\'one two\'", "shell-always-show"), - ("\'one two\'", "shell-escape"), - ("\'one two\'", "shell-escape-always"), + ("'one two'", "shell"), + ("'one two'", "shell-show"), + ("'one two'", "shell-always"), + ("'one two'", "shell-always-show"), + ("'one two'", "shell-escape"), + ("'one two'", "shell-escape-always"), ], ); @@ -551,7 +665,7 @@ mod tests { // One single quote check_names( - "one\'two", + "one'two", &[ ("one'two", "literal"), ("one'two", "literal-show"), @@ -637,7 +751,7 @@ mod tests { ], ); - // The first 16 control characters. NUL is also included, even though it is of + // The first 16 ASCII control characters. NUL is also included, even though it is of // no importance for file names. check_names( "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F", @@ -676,7 +790,7 @@ mod tests { ], ); - // The last 16 control characters. + // The last 16 ASCII control characters. check_names( "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F", &[ @@ -730,6 +844,265 @@ mod tests { ("''$'\\177'", "shell-escape-always"), ], ); + + // The first 16 Unicode control characters. + let test_str = std::str::from_utf8(b"\xC2\x80\xC2\x81\xC2\x82\xC2\x83\xC2\x84\xC2\x85\xC2\x86\xC2\x87\xC2\x88\xC2\x89\xC2\x8A\xC2\x8B\xC2\x8C\xC2\x8D\xC2\x8E\xC2\x8F").unwrap(); + check_names( + test_str, + &[ + ("????????????????", "literal"), + (test_str, "literal-show"), + ("\\302\\200\\302\\201\\302\\202\\302\\203\\302\\204\\302\\205\\302\\206\\302\\207\\302\\210\\302\\211\\302\\212\\302\\213\\302\\214\\302\\215\\302\\216\\302\\217", "escape"), + ("\"\\302\\200\\302\\201\\302\\202\\302\\203\\302\\204\\302\\205\\302\\206\\302\\207\\302\\210\\302\\211\\302\\212\\302\\213\\302\\214\\302\\215\\302\\216\\302\\217\"", "c"), + ("????????????????", "shell"), + (test_str, "shell-show"), + ("'????????????????'", "shell-always"), + (&format!("'{}'", test_str), "shell-always-show"), + ("''$'\\302\\200\\302\\201\\302\\202\\302\\203\\302\\204\\302\\205\\302\\206\\302\\207\\302\\210\\302\\211\\302\\212\\302\\213\\302\\214\\302\\215\\302\\216\\302\\217'", "shell-escape"), + ("''$'\\302\\200\\302\\201\\302\\202\\302\\203\\302\\204\\302\\205\\302\\206\\302\\207\\302\\210\\302\\211\\302\\212\\302\\213\\302\\214\\302\\215\\302\\216\\302\\217'", "shell-escape-always"), + ], + ); + + // The last 16 Unicode control characters. + let test_str = std::str::from_utf8(b"\xC2\x90\xC2\x91\xC2\x92\xC2\x93\xC2\x94\xC2\x95\xC2\x96\xC2\x97\xC2\x98\xC2\x99\xC2\x9A\xC2\x9B\xC2\x9C\xC2\x9D\xC2\x9E\xC2\x9F").unwrap(); + check_names( + test_str, + &[ + ("????????????????", "literal"), + (test_str, "literal-show"), + ("\\302\\220\\302\\221\\302\\222\\302\\223\\302\\224\\302\\225\\302\\226\\302\\227\\302\\230\\302\\231\\302\\232\\302\\233\\302\\234\\302\\235\\302\\236\\302\\237", "escape"), + ("\"\\302\\220\\302\\221\\302\\222\\302\\223\\302\\224\\302\\225\\302\\226\\302\\227\\302\\230\\302\\231\\302\\232\\302\\233\\302\\234\\302\\235\\302\\236\\302\\237\"", "c"), + ("????????????????", "shell"), + (test_str, "shell-show"), + ("'????????????????'", "shell-always"), + (&format!("'{}'", test_str), "shell-always-show"), + ("''$'\\302\\220\\302\\221\\302\\222\\302\\223\\302\\224\\302\\225\\302\\226\\302\\227\\302\\230\\302\\231\\302\\232\\302\\233\\302\\234\\302\\235\\302\\236\\302\\237'", "shell-escape"), + ("''$'\\302\\220\\302\\221\\302\\222\\302\\223\\302\\224\\302\\225\\302\\226\\302\\227\\302\\230\\302\\231\\302\\232\\302\\233\\302\\234\\302\\235\\302\\236\\302\\237'", "shell-escape-always"), + ], + ); + } + + #[test] + fn test_non_unicode_bytes() { + let ascii = b'_'; + let continuation = b'\xA7'; + let first2byte = b'\xC2'; + let first3byte = b'\xE0'; + let first4byte = b'\xF0'; + let invalid = b'\xC0'; + + // a single byte value invalid outside of additional context in UTF-8 + check_names_raw( + &[continuation], + &[ + (b"?", "literal"), + (b"\xA7", "literal-show"), + (b"\\247", "escape"), + (b"\"\\247\"", "c"), + (b"?", "shell"), + (b"\xA7", "shell-show"), + (b"'?'", "shell-always"), + (b"'\xA7'", "shell-always-show"), + (b"''$'\\247'", "shell-escape"), + (b"''$'\\247'", "shell-escape-always"), + ], + ); + + // ...but the byte becomes valid with appropriate context + // (this is just the § character in UTF-8, written as bytes) + check_names_raw( + &[first2byte, continuation], + &[ + (b"\xC2\xA7", "literal"), + (b"\xC2\xA7", "literal-show"), + (b"\xC2\xA7", "escape"), + (b"\"\xC2\xA7\"", "c"), + (b"\xC2\xA7", "shell"), + (b"\xC2\xA7", "shell-show"), + (b"'\xC2\xA7'", "shell-always"), + (b"'\xC2\xA7'", "shell-always-show"), + (b"\xC2\xA7", "shell-escape"), + (b"'\xC2\xA7'", "shell-escape-always"), + ], + ); + + // mixed with valid characters + check_names_raw( + &[continuation, ascii], + &[ + (b"?_", "literal"), + (b"\xA7_", "literal-show"), + (b"\\247_", "escape"), + (b"\"\\247_\"", "c"), + (b"?_", "shell"), + (b"\xA7_", "shell-show"), + (b"'?_'", "shell-always"), + (b"'\xA7_'", "shell-always-show"), + (b"''$'\\247''_'", "shell-escape"), + (b"''$'\\247''_'", "shell-escape-always"), + ], + ); + check_names_raw( + &[ascii, continuation], + &[ + (b"_?", "literal"), + (b"_\xA7", "literal-show"), + (b"_\\247", "escape"), + (b"\"_\\247\"", "c"), + (b"_?", "shell"), + (b"_\xA7", "shell-show"), + (b"'_?'", "shell-always"), + (b"'_\xA7'", "shell-always-show"), + (b"'_'$'\\247'", "shell-escape"), + (b"'_'$'\\247'", "shell-escape-always"), + ], + ); + check_names_raw( + &[ascii, continuation, ascii], + &[ + (b"_?_", "literal"), + (b"_\xA7_", "literal-show"), + (b"_\\247_", "escape"), + (b"\"_\\247_\"", "c"), + (b"_?_", "shell"), + (b"_\xA7_", "shell-show"), + (b"'_?_'", "shell-always"), + (b"'_\xA7_'", "shell-always-show"), + (b"'_'$'\\247''_'", "shell-escape"), + (b"'_'$'\\247''_'", "shell-escape-always"), + ], + ); + check_names_raw( + &[continuation, ascii, continuation], + &[ + (b"?_?", "literal"), + (b"\xA7_\xA7", "literal-show"), + (b"\\247_\\247", "escape"), + (b"\"\\247_\\247\"", "c"), + (b"?_?", "shell"), + (b"\xA7_\xA7", "shell-show"), + (b"'?_?'", "shell-always"), + (b"'\xA7_\xA7'", "shell-always-show"), + (b"''$'\\247''_'$'\\247'", "shell-escape"), + (b"''$'\\247''_'$'\\247'", "shell-escape-always"), + ], + ); + + // contiguous invalid bytes + check_names_raw( + &[ + ascii, + invalid, + ascii, + continuation, + continuation, + ascii, + continuation, + continuation, + continuation, + ascii, + continuation, + continuation, + continuation, + continuation, + ascii, + ], + &[ + (b"_?_??_???_????_", "literal"), + ( + b"_\xC0_\xA7\xA7_\xA7\xA7\xA7_\xA7\xA7\xA7\xA7_", + "literal-show", + ), + ( + b"_\\300_\\247\\247_\\247\\247\\247_\\247\\247\\247\\247_", + "escape", + ), + ( + b"\"_\\300_\\247\\247_\\247\\247\\247_\\247\\247\\247\\247_\"", + "c", + ), + (b"_?_??_???_????_", "shell"), + ( + b"_\xC0_\xA7\xA7_\xA7\xA7\xA7_\xA7\xA7\xA7\xA7_", + "shell-show", + ), + (b"'_?_??_???_????_'", "shell-always"), + ( + b"'_\xC0_\xA7\xA7_\xA7\xA7\xA7_\xA7\xA7\xA7\xA7_'", + "shell-always-show", + ), + ( + b"'_'$'\\300''_'$'\\247\\247''_'$'\\247\\247\\247''_'$'\\247\\247\\247\\247''_'", + "shell-escape", + ), + ( + b"'_'$'\\300''_'$'\\247\\247''_'$'\\247\\247\\247''_'$'\\247\\247\\247\\247''_'", + "shell-escape-always", + ), + ], + ); + + // invalid multi-byte sequences that start valid + check_names_raw( + &[first2byte, ascii], + &[ + (b"?_", "literal"), + (b"\xC2_", "literal-show"), + (b"\\302_", "escape"), + (b"\"\\302_\"", "c"), + (b"?_", "shell"), + (b"\xC2_", "shell-show"), + (b"'?_'", "shell-always"), + (b"'\xC2_'", "shell-always-show"), + (b"''$'\\302''_'", "shell-escape"), + (b"''$'\\302''_'", "shell-escape-always"), + ], + ); + check_names_raw( + &[first2byte, first2byte, continuation], + &[ + (b"?\xC2\xA7", "literal"), + (b"\xC2\xC2\xA7", "literal-show"), + (b"\\302\xC2\xA7", "escape"), + (b"\"\\302\xC2\xA7\"", "c"), + (b"?\xC2\xA7", "shell"), + (b"\xC2\xC2\xA7", "shell-show"), + (b"'?\xC2\xA7'", "shell-always"), + (b"'\xC2\xC2\xA7'", "shell-always-show"), + (b"''$'\\302''\xC2\xA7'", "shell-escape"), + (b"''$'\\302''\xC2\xA7'", "shell-escape-always"), + ], + ); + check_names_raw( + &[first3byte, continuation, ascii], + &[ + (b"??_", "literal"), + (b"\xE0\xA7_", "literal-show"), + (b"\\340\\247_", "escape"), + (b"\"\\340\\247_\"", "c"), + (b"??_", "shell"), + (b"\xE0\xA7_", "shell-show"), + (b"'??_'", "shell-always"), + (b"'\xE0\xA7_'", "shell-always-show"), + (b"''$'\\340\\247''_'", "shell-escape"), + (b"''$'\\340\\247''_'", "shell-escape-always"), + ], + ); + check_names_raw( + &[first4byte, continuation, continuation, ascii], + &[ + (b"???_", "literal"), + (b"\xF0\xA7\xA7_", "literal-show"), + (b"\\360\\247\\247_", "escape"), + (b"\"\\360\\247\\247_\"", "c"), + (b"???_", "shell"), + (b"\xF0\xA7\xA7_", "shell-show"), + (b"'???_'", "shell-always"), + (b"'\xF0\xA7\xA7_'", "shell-always-show"), + (b"''$'\\360\\247\\247''_'", "shell-escape"), + (b"''$'\\360\\247\\247''_'", "shell-escape-always"), + ], + ); } #[test] @@ -765,7 +1138,7 @@ mod tests { ("one\\\\two", "escape"), ("\"one\\\\two\"", "c"), ("'one\\two'", "shell"), - ("\'one\\two\'", "shell-always"), + ("'one\\two'", "shell-always"), ("'one\\two'", "shell-escape"), ("'one\\two'", "shell-escape-always"), ], diff --git a/src/uucore/src/lib/features/ranges.rs b/src/uucore/src/lib/features/ranges.rs index 222be7ca3..88851b9aa 100644 --- a/src/uucore/src/lib/features/ranges.rs +++ b/src/uucore/src/lib/features/ranges.rs @@ -91,7 +91,7 @@ impl Range { Ok(Self::merge(ranges)) } - /// Merge any overlapping ranges + /// Merge any overlapping ranges. Adjacent ranges are *NOT* merged. /// /// Is guaranteed to return only disjoint ranges in a sorted order. fn merge(mut ranges: Vec) -> Vec { @@ -101,10 +101,7 @@ impl Range { for i in 0..ranges.len() { let j = i + 1; - // The +1 is a small optimization, because we can merge adjacent Ranges. - // For example (1,3) and (4,6), because in the integers, there are no - // possible values between 3 and 4, this is equivalent to (1,6). - while j < ranges.len() && ranges[j].low <= ranges[i].high + 1 { + while j < ranges.len() && ranges[j].low <= ranges[i].high { let j_high = ranges.remove(j).high; ranges[i].high = max(ranges[i].high, j_high); } @@ -216,8 +213,8 @@ mod test { &[r(10, 40), r(50, 60)], ); - // Merge adjacent ranges - m(vec![r(1, 3), r(4, 6)], &[r(1, 6)]); + // Don't merge adjacent ranges + m(vec![r(1, 3), r(4, 6)], &[r(1, 3), r(4, 6)]); } #[test] diff --git a/src/uucore/src/lib/features/sum.rs b/src/uucore/src/lib/features/sum.rs index 086c6ca9d..df9e1673d 100644 --- a/src/uucore/src/lib/features/sum.rs +++ b/src/uucore/src/lib/features/sum.rs @@ -207,13 +207,6 @@ impl Digest for CRC { } } -// This can be replaced with usize::div_ceil once it is stabilized. -// This implementation approach is optimized for when `b` is a constant, -// particularly a power of two. -pub fn div_ceil(a: usize, b: usize) -> usize { - (a + b - 1) / b -} - pub struct BSD { state: u16, } @@ -410,7 +403,7 @@ impl<'a> DigestWriter<'a> { } } -impl<'a> Write for DigestWriter<'a> { +impl Write for DigestWriter<'_> { #[cfg(not(windows))] fn write(&mut self, buf: &[u8]) -> std::io::Result { self.digest.hash_update(buf); diff --git a/src/uucore/src/lib/lib.rs b/src/uucore/src/lib/lib.rs index 6142e688d..3a6a537ad 100644 --- a/src/uucore/src/lib/lib.rs +++ b/src/uucore/src/lib/lib.rs @@ -70,11 +70,13 @@ pub use crate::features::version_cmp; #[cfg(all(not(windows), feature = "mode"))] pub use crate::features::mode; // ** unix-only +#[cfg(all(any(target_os = "linux", target_os = "android"), feature = "buf-copy"))] +pub use crate::features::buf_copy; #[cfg(all(unix, feature = "entries"))] pub use crate::features::entries; #[cfg(all(unix, feature = "perms"))] pub use crate::features::perms; -#[cfg(all(unix, feature = "pipes"))] +#[cfg(all(unix, any(feature = "pipes", feature = "buf-copy")))] pub use crate::features::pipes; #[cfg(all(unix, feature = "process"))] pub use crate::features::process; @@ -97,7 +99,7 @@ pub use crate::features::wide; #[cfg(feature = "fsext")] pub use crate::features::fsext; -#[cfg(all(unix, not(target_os = "macos"), feature = "fsxattr"))] +#[cfg(all(unix, feature = "fsxattr"))] pub use crate::features::fsxattr; //## core functions @@ -253,9 +255,10 @@ pub fn read_yes() -> bool { } } -/// Helper function for processing delimiter values (which could be non UTF-8) -/// It converts OsString to &[u8] for unix targets only -/// On non-unix (i.e. Windows) it will just return an error if delimiter value is not UTF-8 +/// Converts an `OsStr` to a UTF-8 `&[u8]`. +/// +/// This always succeeds on unix platforms, +/// and fails on other platforms if the string can't be coerced to UTF-8. pub fn os_str_as_bytes(os_string: &OsStr) -> mods::error::UResult<&[u8]> { #[cfg(unix)] let bytes = os_string.as_bytes(); @@ -271,13 +274,28 @@ pub fn os_str_as_bytes(os_string: &OsStr) -> mods::error::UResult<&[u8]> { Ok(bytes) } -/// Helper function for converting a slice of bytes into an &OsStr -/// or OsString in non-unix targets. +/// Performs a potentially lossy conversion from `OsStr` to UTF-8 bytes. /// -/// It converts `&[u8]` to `Cow` for unix targets only. -/// On non-unix (i.e. Windows), the conversion goes through the String type -/// and thus undergo UTF-8 validation, making it fail if the stream contains -/// non-UTF-8 characters. +/// This is always lossless on unix platforms, +/// and wraps [`OsStr::to_string_lossy`] on non-unix platforms. +pub fn os_str_as_bytes_lossy(os_string: &OsStr) -> Cow<[u8]> { + #[cfg(unix)] + let bytes = Cow::from(os_string.as_bytes()); + + #[cfg(not(unix))] + let bytes = match os_string.to_string_lossy() { + Cow::Borrowed(slice) => Cow::from(slice.as_bytes()), + Cow::Owned(owned) => Cow::from(owned.into_bytes()), + }; + + bytes +} + +/// Converts a `&[u8]` to an `&OsStr`, +/// or parses it as UTF-8 into an [`OsString`] on non-unix platforms. +/// +/// This always succeeds on unix platforms, +/// and fails on other platforms if the bytes can't be parsed as UTF-8. pub fn os_str_from_bytes(bytes: &[u8]) -> mods::error::UResult> { #[cfg(unix)] let os_str = Cow::Borrowed(OsStr::from_bytes(bytes)); @@ -289,9 +307,10 @@ pub fn os_str_from_bytes(bytes: &[u8]) -> mods::error::UResult> { Ok(os_str) } -/// Helper function for making an `OsString` from a byte field -/// It converts `Vec` to `OsString` for unix targets only. -/// On non-unix (i.e. Windows) it may fail if the bytes are not valid UTF-8 +/// Converts a `Vec` into an `OsString`, parsing as UTF-8 on non-unix platforms. +/// +/// This always succeeds on unix platforms, +/// and fails on other platforms if the bytes can't be parsed as UTF-8. pub fn os_string_from_vec(vec: Vec) -> mods::error::UResult { #[cfg(unix)] let s = OsString::from_vec(vec); diff --git a/src/uuhelp_parser/src/lib.rs b/src/uuhelp_parser/src/lib.rs index da50c037b..0e0907f8a 100644 --- a/src/uuhelp_parser/src/lib.rs +++ b/src/uuhelp_parser/src/lib.rs @@ -73,7 +73,7 @@ pub fn parse_usage(content: &str) -> String { pub fn parse_section(section: &str, content: &str) -> Option { fn is_section_header(line: &str, section: &str) -> bool { line.strip_prefix("##") - .map_or(false, |l| l.trim().to_lowercase() == section) + .is_some_and(|l| l.trim().to_lowercase() == section) } let section = §ion.to_lowercase(); diff --git a/tests/by-util/test_base64.rs b/tests/by-util/test_base64.rs index f07da925f..29b9edf02 100644 --- a/tests/by-util/test_base64.rs +++ b/tests/by-util/test_base64.rs @@ -40,6 +40,28 @@ fn test_encode_repeat_flags_later_wrap_15() { .stdout_only("aGVsbG8sIHdvcmx\nkIQ==\n"); // spell-checker:disable-line } +#[test] +fn test_decode_short() { + let input = "aQ"; + new_ucmd!() + .args(&["--decode"]) + .pipe_in(input) + .succeeds() + .stdout_only("i"); +} + +#[test] +fn test_multi_lines() { + let input = ["aQ\n\n\n", "a\nQ==\n\n\n"]; + for i in input { + new_ucmd!() + .args(&["--decode"]) + .pipe_in(i) + .succeeds() + .stdout_only("i"); + } +} + #[test] fn test_base64_encode_file() { new_ucmd!() @@ -105,6 +127,17 @@ fn test_wrap() { // spell-checker:disable-next-line .stdout_only("VGhlIHF1aWNrIGJyb3du\nIGZveCBqdW1wcyBvdmVy\nIHRoZSBsYXp5IGRvZy4=\n"); } + let input = "hello, world"; + new_ucmd!() + .args(&["--wrap", "0"]) + .pipe_in(input) + .succeeds() + .stdout_only("aGVsbG8sIHdvcmxk"); // spell-checker:disable-line + new_ucmd!() + .args(&["--wrap", "30"]) + .pipe_in(input) + .succeeds() + .stdout_only("aGVsbG8sIHdvcmxk\n"); // spell-checker:disable-line } #[test] diff --git a/tests/by-util/test_basenc.rs b/tests/by-util/test_basenc.rs index 85c05ad3e..c0f40cd1d 100644 --- a/tests/by-util/test_basenc.rs +++ b/tests/by-util/test_basenc.rs @@ -130,6 +130,24 @@ fn test_base16_decode() { .stdout_only("Hello, World!"); } +#[test] +fn test_base16_decode_lowercase() { + new_ucmd!() + .args(&["--base16", "-d"]) + .pipe_in("48656c6c6f2c20576f726c6421") + .succeeds() + .stdout_only("Hello, World!"); +} + +#[test] +fn test_base16_decode_and_ignore_garbage_lowercase() { + new_ucmd!() + .args(&["--base16", "-d", "-i"]) + .pipe_in("48656c6c6f2c20576f726c6421") + .succeeds() + .stdout_only("Hello, World!"); +} + #[test] fn test_base2msbf() { new_ucmd!() diff --git a/tests/by-util/test_cksum.rs b/tests/by-util/test_cksum.rs index 98366cbec..2efc78b96 100644 --- a/tests/by-util/test_cksum.rs +++ b/tests/by-util/test_cksum.rs @@ -2,7 +2,7 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (words) asdf algo algos asha mgmt xffname +// spell-checker:ignore (words) asdf algo algos asha mgmt xffname hexa GFYEQ HYQK Yqxb use crate::common::util::TestScenario; @@ -1251,33 +1251,6 @@ fn test_several_files_error_mgmt() { .stderr_contains("incorrect: no properly "); } -#[cfg(target_os = "linux")] -#[test] -fn test_non_utf8_filename() { - use std::ffi::OsString; - use std::os::unix::ffi::OsStringExt; - - let scene = TestScenario::new(util_name!()); - let at = &scene.fixtures; - let filename: OsString = OsStringExt::from_vec(b"funky\xffname".to_vec()); - - at.touch(&filename); - - scene - .ucmd() - .arg(&filename) - .succeeds() - .stdout_is_bytes(b"4294967295 0 funky\xffname\n") - .no_stderr(); - scene - .ucmd() - .arg("-asha256") - .arg(filename) - .succeeds() - .stdout_is_bytes(b"SHA256 (funky\xffname) = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\n") - .no_stderr(); -} - #[test] fn test_check_comment_line() { // A comment in a checksum file shall be discarded unnoticed. @@ -1430,12 +1403,13 @@ fn test_check_trailing_space_fails() { /// in checksum files. /// These tests are excluded from Windows because it does not provide any safe /// conversion between `OsString` and byte sequences for non-utf-8 strings. -#[cfg(not(windows))] mod check_utf8 { - use super::*; + // This test should pass on linux and macos. + #[cfg(not(windows))] #[test] fn test_check_non_utf8_comment() { + use super::*; let hashes = b"MD5 (empty) = 1B2M2Y8AsgTpgAmY7PhCfg==\n\ # Comment with a non utf8 char: >>\xff<<\n\ @@ -1458,15 +1432,18 @@ mod check_utf8 { .no_stderr(); } + // This test should pass on linux. Windows and macos will fail to + // create a file which name contains '\xff'. #[cfg(target_os = "linux")] #[test] fn test_check_non_utf8_filename() { + use super::*; use std::{ffi::OsString, os::unix::ffi::OsStringExt}; let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; let filename: OsString = OsStringExt::from_vec(b"funky\xffname".to_vec()); - at.touch(&filename); + at.touch(filename); // Checksum match at.write_bytes("check", @@ -1502,3 +1479,368 @@ mod check_utf8 { .stderr_contains("1 listed file could not be read"); } } + +#[test] +fn test_check_blake_length_guess() { + let correct_lines = [ + // Correct: The length is not explicit, but the checksum's size + // matches the default parameter. + "BLAKE2b (foo.dat) = ca002330e69d3e6b84a46a56a6533fd79d51d97a3bb7cad6c2ff43b354185d6dc1e723fb3db4ae0737e120378424c714bb982d9dc5bbd7a0ab318240ddd18f8d", + // Correct: The length is explicitly given, and the checksum's size + // matches the length. + "BLAKE2b-512 (foo.dat) = ca002330e69d3e6b84a46a56a6533fd79d51d97a3bb7cad6c2ff43b354185d6dc1e723fb3db4ae0737e120378424c714bb982d9dc5bbd7a0ab318240ddd18f8d", + // Correct: the checksum size is not default but + // the length is explicitly given. + "BLAKE2b-48 (foo.dat) = 171cdfdf84ed", + ]; + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.write("foo.dat", "foo"); + + for line in correct_lines { + at.write("foo.sums", line); + scene + .ucmd() + .arg("--check") + .arg(at.subdir.join("foo.sums")) + .succeeds() + .stdout_is("foo.dat: OK\n"); + } + + // Incorrect lines + + // This is incorrect because the algorithm provides no length, + // and the checksum length is not default. + let incorrect = "BLAKE2b (foo.dat) = 171cdfdf84ed"; + at.write("foo.sums", incorrect); + scene + .ucmd() + .arg("--check") + .arg(at.subdir.join("foo.sums")) + .fails() + .stderr_contains("foo.sums: no properly formatted checksum lines found"); +} + +#[test] +fn test_check_confusing_base64() { + let cksum = "BLAKE2b-48 (foo.dat) = fc1f97C4"; + + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.write("foo.dat", "esq"); + at.write("foo.sums", cksum); + + scene + .ucmd() + .arg("--check") + .arg(at.subdir.join("foo.sums")) + .succeeds() + .stdout_is("foo.dat: OK\n"); +} + +/// This test checks that when a file contains several checksum lines +/// with different encoding, the decoding still works. +#[test] +fn test_check_mix_hex_base64() { + let b64 = "BLAKE2b-128 (foo1.dat) = BBNuJPhdRwRlw9tm5Y7VbA=="; + let hex = "BLAKE2b-128 (foo2.dat) = 04136e24f85d470465c3db66e58ed56c"; + + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.write("foo1.dat", "foo"); + at.write("foo2.dat", "foo"); + + at.write("hex_b64", &format!("{hex}\n{b64}")); + at.write("b64_hex", &format!("{b64}\n{hex}")); + + scene + .ucmd() + .arg("--check") + .arg(at.subdir.join("hex_b64")) + .succeeds() + .stdout_only("foo2.dat: OK\nfoo1.dat: OK\n"); + + scene + .ucmd() + .arg("--check") + .arg(at.subdir.join("b64_hex")) + .succeeds() + .stdout_only("foo1.dat: OK\nfoo2.dat: OK\n"); +} + +/// This test ensures that an improperly formatted base64 checksum in a file +/// does not interrupt the processing of next lines. +#[test] +fn test_check_incorrectly_formatted_checksum_keeps_processing_b64() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.touch("f"); + + let good_ck = "MD5 (f) = 1B2M2Y8AsgTpgAmY7PhCfg=="; // OK + let bad_ck = "MD5 (f) = 1B2M2Y8AsgTpgAmY7PhCfg="; // Missing last '=' + + // Good then Bad + scene + .ucmd() + .arg("--check") + .pipe_in([good_ck, bad_ck].join("\n").as_bytes().to_vec()) + .succeeds() + .stdout_contains("f: OK") + .stderr_contains("cksum: WARNING: 1 line is improperly formatted"); + + // Bad then Good + scene + .ucmd() + .arg("--check") + .pipe_in([bad_ck, good_ck].join("\n").as_bytes().to_vec()) + .succeeds() + .stdout_contains("f: OK") + .stderr_contains("cksum: WARNING: 1 line is improperly formatted"); +} + +/// This test ensures that an improperly formatted hexadecimal checksum in a +/// file does not interrupt the processing of next lines. +#[test] +fn test_check_incorrectly_formatted_checksum_keeps_processing_hex() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.touch("f"); + + let good_ck = "MD5 (f) = d41d8cd98f00b204e9800998ecf8427e"; // OK + let bad_ck = "MD5 (f) = d41d8cd98f00b204e9800998ecf8427"; // Missing last + + // Good then Bad + scene + .ucmd() + .arg("--check") + .pipe_in([good_ck, bad_ck].join("\n").as_bytes().to_vec()) + .succeeds() + .stdout_contains("f: OK") + .stderr_contains("cksum: WARNING: 1 line is improperly formatted"); + + // Bad then Good + scene + .ucmd() + .arg("--check") + .pipe_in([bad_ck, good_ck].join("\n").as_bytes().to_vec()) + .succeeds() + .stdout_contains("f: OK") + .stderr_contains("cksum: WARNING: 1 line is improperly formatted"); +} + +/// This module reimplements the cksum-base64.pl GNU test. +mod gnu_cksum_base64 { + use super::*; + use crate::common::util::log_info; + + const PAIRS: [(&str, &str); 11] = [ + ("sysv", "0 0 f"), + ("bsd", "00000 0 f"), + ("crc", "4294967295 0 f"), + ("md5", "1B2M2Y8AsgTpgAmY7PhCfg=="), + ("sha1", "2jmj7l5rSw0yVb/vlWAYkK/YBwk="), + ("sha224", "0UoCjCo6K8lHYQK7KII0xBWisB+CjqYqxbPkLw=="), + ("sha256", "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU="), + ( + "sha384", + "OLBgp1GsljhM2TJ+sbHjaiH9txEUvgdDTAzHv2P24donTt6/529l+9Ua0vFImLlb", + ), + ( + "sha512", + "z4PhNX7vuL3xVChQ1m2AB9Yg5AULVxXcg/SpIdNs6c5H0NE8XYXysP+DGNKHfuwvY7kxvUdBeoGlODJ6+SfaPg==" + ), + ( + "blake2b", + "eGoC90IBWQPGxv2FJVLScpEvR0DhWEdhiobiF/cfVBnSXhAxr+5YUxOJZESTTrBLkDpoWxRIt1XVb3Aa/pvizg==" + ), + ("sm3", "GrIdg1XPoX+OYRlIMegajyK+yMco/vt0ftA161CCqis="), + ]; + + fn make_scene() -> TestScenario { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.touch("f"); + + scene + } + + fn output_format(algo: &str, digest: &str) -> String { + if ["sysv", "bsd", "crc"].contains(&algo) { + digest.to_string() + } else { + format!("{} (f) = {}", algo.to_uppercase(), digest).replace("BLAKE2B", "BLAKE2b") + } + } + + #[test] + fn test_generating() { + // Ensure that each algorithm works with `--base64`. + let scene = make_scene(); + + for (algo, digest) in PAIRS { + scene + .ucmd() + .arg("--base64") + .arg("-a") + .arg(algo) + .arg("f") + .succeeds() + .stdout_only(format!("{}\n", output_format(algo, digest))); + } + } + + #[test] + fn test_chk() { + // For each algorithm that accepts `--check`, + // ensure that it works with base64 digests. + let scene = make_scene(); + + for (algo, digest) in PAIRS { + if ["sysv", "bsd", "crc"].contains(&algo) { + // These algorithms do not accept `--check` + continue; + } + + let line = output_format(algo, digest); + scene + .ucmd() + .arg("--check") + .arg("--strict") + .pipe_in(line) + .succeeds() + .stdout_only("f: OK\n"); + } + } + + #[test] + fn test_chk_eq1() { + // For digests ending with '=', ensure `--check` fails if '=' is removed. + let scene = make_scene(); + + for (algo, digest) in PAIRS { + if !digest.ends_with('=') { + continue; + } + + let mut line = output_format(algo, digest); + if line.ends_with('=') { + line.pop(); + } + + log_info(format!("ALGORITHM: {algo}, STDIN: '{line}'"), ""); + scene + .ucmd() + .arg("--check") + .pipe_in(line) + .fails() + .no_stdout() + .stderr_contains("no properly formatted checksum lines found"); + } + } + + #[test] + fn test_chk_eq2() { + // For digests ending with '==', + // ensure `--check` fails if '==' is removed. + let scene = make_scene(); + + for (algo, digest) in PAIRS { + if !digest.ends_with("==") { + continue; + } + + let line = output_format(algo, digest); + let line = line.trim_end_matches("=="); + + log_info(format!("ALGORITHM: {algo}, STDIN: '{line}'"), ""); + scene + .ucmd() + .arg("--check") + .pipe_in(line) + .fails() + .no_stdout() + .stderr_contains("no properly formatted checksum lines found"); + } + } +} + +/// The tests in this module check the behavior of cksum when given different +/// checksum formats and algorithms in the same file, while specifying an +/// algorithm on CLI or not. +mod format_mix { + use super::*; + + // First line is algo-based, second one is not + const INPUT_ALGO_NON_ALGO: &str = "\ + BLAKE2b (bar) = 786a02f742015903c6c6fd852552d272912f4740e15847618a86e217f71f5419d25e1031afee585313896444934eb04b903a685b1448b755d56f701afe9be2ce\n\ + 786a02f742015903c6c6fd852552d272912f4740e15847618a86e217f71f5419d25e1031afee585313896444934eb04b903a685b1448b755d56f701afe9be2ce foo"; + + // First line is non algo-based, second one is + const INPUT_NON_ALGO_ALGO: &str = "\ + 786a02f742015903c6c6fd852552d272912f4740e15847618a86e217f71f5419d25e1031afee585313896444934eb04b903a685b1448b755d56f701afe9be2ce foo\n\ + BLAKE2b (bar) = 786a02f742015903c6c6fd852552d272912f4740e15847618a86e217f71f5419d25e1031afee585313896444934eb04b903a685b1448b755d56f701afe9be2ce"; + + /// Make a simple scene with foo and bar empty files + fn make_scene() -> TestScenario { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.touch("foo"); + at.touch("bar"); + + scene + } + + #[test] + fn test_check_cli_algo_non_algo() { + let scene = make_scene(); + scene + .ucmd() + .arg("--check") + .arg("--algo=blake2b") + .pipe_in(INPUT_ALGO_NON_ALGO) + .succeeds() + .stdout_contains("bar: OK\nfoo: OK") + .no_stderr(); + } + + #[test] + fn test_check_cli_non_algo_algo() { + let scene = make_scene(); + scene + .ucmd() + .arg("--check") + .arg("--algo=blake2b") + .pipe_in(INPUT_NON_ALGO_ALGO) + .succeeds() + .stdout_contains("foo: OK\nbar: OK") + .no_stderr(); + } + + #[test] + fn test_check_algo_non_algo() { + let scene = make_scene(); + scene + .ucmd() + .arg("--check") + .pipe_in(INPUT_ALGO_NON_ALGO) + .succeeds() + .stdout_contains("bar: OK") + .stderr_contains("cksum: WARNING: 1 line is improperly formatted"); + } + + #[test] + fn test_check_non_algo_algo() { + let scene = make_scene(); + scene + .ucmd() + .arg("--check") + .pipe_in(INPUT_NON_ALGO_ALGO) + .succeeds() + .stdout_contains("bar: OK") + .stderr_contains("cksum: WARNING: 1 line is improperly formatted"); + } +} diff --git a/tests/by-util/test_comm.rs b/tests/by-util/test_comm.rs index 2dc385ef3..b62febf50 100644 --- a/tests/by-util/test_comm.rs +++ b/tests/by-util/test_comm.rs @@ -292,3 +292,36 @@ fn test_no_such_file() { .fails() .stderr_only("comm: bogus_file_1: No such file or directory\n"); } + +#[test] +fn test_is_dir() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + scene + .ucmd() + .args(&[".", "."]) + .fails() + .stderr_only("comm: .: Is a directory\n"); + + at.mkdir("dir"); + scene + .ucmd() + .args(&["dir", "."]) + .fails() + .stderr_only("comm: dir: Is a directory\n"); + + at.touch("file"); + scene + .ucmd() + .args(&[".", "file"]) + .fails() + .stderr_only("comm: .: Is a directory\n"); + + at.touch("file"); + scene + .ucmd() + .args(&["file", "."]) + .fails() + .stderr_only("comm: .: Is a directory\n"); +} diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index 156daec1f..7a0889b0f 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -8,6 +8,7 @@ use crate::common::util::TestScenario; #[cfg(not(windows))] use std::fs::set_permissions; +use std::io::Write; #[cfg(not(windows))] use std::os::unix::fs; @@ -447,9 +448,9 @@ fn test_cp_arg_update_older_dest_older_than_src() { let old_content = "old content\n"; let new_content = "new content\n"; - at.write(old, old_content); - - sleep(Duration::from_secs(1)); + let mut f = at.make_file(old); + f.write_all(old_content.as_bytes()).unwrap(); + f.set_modified(std::time::UNIX_EPOCH).unwrap(); at.write(new, new_content); @@ -473,9 +474,9 @@ fn test_cp_arg_update_short_no_overwrite() { let old_content = "old content\n"; let new_content = "new content\n"; - at.write(old, old_content); - - sleep(Duration::from_secs(1)); + let mut f = at.make_file(old); + f.write_all(old_content.as_bytes()).unwrap(); + f.set_modified(std::time::UNIX_EPOCH).unwrap(); at.write(new, new_content); @@ -499,9 +500,9 @@ fn test_cp_arg_update_short_overwrite() { let old_content = "old content\n"; let new_content = "new content\n"; - at.write(old, old_content); - - sleep(Duration::from_secs(1)); + let mut f = at.make_file(old); + f.write_all(old_content.as_bytes()).unwrap(); + f.set_modified(std::time::UNIX_EPOCH).unwrap(); at.write(new, new_content); @@ -526,9 +527,9 @@ fn test_cp_arg_update_none_then_all() { let old_content = "old content\n"; let new_content = "new content\n"; - at.write(old, old_content); - - sleep(Duration::from_secs(1)); + let mut f = at.make_file(old); + f.write_all(old_content.as_bytes()).unwrap(); + f.set_modified(std::time::UNIX_EPOCH).unwrap(); at.write(new, new_content); @@ -554,9 +555,9 @@ fn test_cp_arg_update_all_then_none() { let old_content = "old content\n"; let new_content = "new content\n"; - at.write(old, old_content); - - sleep(Duration::from_secs(1)); + let mut f = at.make_file(old); + f.write_all(old_content.as_bytes()).unwrap(); + f.set_modified(std::time::UNIX_EPOCH).unwrap(); at.write(new, new_content); diff --git a/tests/by-util/test_csplit.rs b/tests/by-util/test_csplit.rs index 03b8c92fc..231571522 100644 --- a/tests/by-util/test_csplit.rs +++ b/tests/by-util/test_csplit.rs @@ -130,17 +130,21 @@ fn test_up_to_match_sequence() { #[test] fn test_up_to_match_offset() { - let (at, mut ucmd) = at_and_ucmd!(); - ucmd.args(&["numbers50.txt", "/9$/+3"]) - .succeeds() - .stdout_only("24\n117\n"); + for offset in ["3", "+3"] { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", &format!("/9$/{offset}")]) + .succeeds() + .stdout_only("24\n117\n"); - let count = glob(&at.plus_as_string("xx*")) - .expect("there should be splits created") - .count(); - assert_eq!(count, 2); - assert_eq!(at.read("xx00"), generate(1, 12)); - assert_eq!(at.read("xx01"), generate(12, 51)); + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 2); + assert_eq!(at.read("xx00"), generate(1, 12)); + assert_eq!(at.read("xx01"), generate(12, 51)); + at.remove("xx00"); + at.remove("xx01"); + } } #[test] @@ -316,16 +320,19 @@ fn test_skip_to_match_sequence4() { #[test] fn test_skip_to_match_offset() { - let (at, mut ucmd) = at_and_ucmd!(); - ucmd.args(&["numbers50.txt", "%23%+3"]) - .succeeds() - .stdout_only("75\n"); + for offset in ["3", "+3"] { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", &format!("%23%{offset}")]) + .succeeds() + .stdout_only("75\n"); - let count = glob(&at.plus_as_string("xx*")) - .expect("there should be splits created") - .count(); - assert_eq!(count, 1); - assert_eq!(at.read("xx00"), generate(26, 51)); + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 1); + assert_eq!(at.read("xx00"), generate(26, 51)); + at.remove("xx00"); + } } #[test] @@ -387,18 +394,23 @@ fn test_option_keep() { #[test] fn test_option_quiet() { - let (at, mut ucmd) = at_and_ucmd!(); - ucmd.args(&["--quiet", "numbers50.txt", "13", "%25%", "/0$/"]) - .succeeds() - .no_stdout(); + for arg in ["-q", "--quiet", "-s", "--silent"] { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&[arg, "numbers50.txt", "13", "%25%", "/0$/"]) + .succeeds() + .no_stdout(); - let count = glob(&at.plus_as_string("xx*")) - .expect("there should be splits created") - .count(); - assert_eq!(count, 3); - assert_eq!(at.read("xx00"), generate(1, 13)); - assert_eq!(at.read("xx01"), generate(25, 30)); - assert_eq!(at.read("xx02"), generate(30, 51)); + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 3); + assert_eq!(at.read("xx00"), generate(1, 13)); + assert_eq!(at.read("xx01"), generate(25, 30)); + assert_eq!(at.read("xx02"), generate(30, 51)); + at.remove("xx00"); + at.remove("xx01"); + at.remove("xx02"); + } } #[test] diff --git a/tests/by-util/test_cut.rs b/tests/by-util/test_cut.rs index 86d3ddf0f..dbd26abb2 100644 --- a/tests/by-util/test_cut.rs +++ b/tests/by-util/test_cut.rs @@ -2,6 +2,9 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. + +// spell-checker:ignore defg + use crate::common::util::TestScenario; static INPUT: &str = "lists.txt"; @@ -43,6 +46,13 @@ static COMPLEX_SEQUENCE: &TestedSequence = &TestedSequence { sequence: "9-,6-7,-2,4", }; +#[test] +fn test_no_args() { + new_ucmd!().fails().stderr_is( + "cut: invalid usage: expects one of --fields (-f), --chars (-c) or --bytes (-b)\n", + ); +} + #[test] fn test_invalid_arg() { new_ucmd!().arg("--definitely-invalid").fails().code_is(1); @@ -246,25 +256,29 @@ fn test_no_such_file() { } #[test] -fn test_equal_as_delimiter1() { - new_ucmd!() - .args(&["-f", "2", "-d="]) - .pipe_in("--dir=./out/lib") - .succeeds() - .stdout_only("./out/lib\n"); +fn test_equal_as_delimiter() { + for arg in ["-d=", "--delimiter=="] { + new_ucmd!() + .args(&["-f2", arg]) + .pipe_in("--dir=./out/lib") + .succeeds() + .stdout_only("./out/lib\n"); + } } #[test] -fn test_equal_as_delimiter2() { - new_ucmd!() - .args(&["-f2", "--delimiter="]) - .pipe_in("a=b\n") - .succeeds() - .stdout_only("a=b\n"); +fn test_empty_string_as_delimiter() { + for arg in ["-d''", "--delimiter=", "--delimiter=''"] { + new_ucmd!() + .args(&["-f2", arg]) + .pipe_in("a\0b\n") + .succeeds() + .stdout_only("b\n"); + } } #[test] -fn test_equal_as_delimiter3() { +fn test_empty_string_as_delimiter_with_output_delimiter() { new_ucmd!() .args(&["-f", "1,2", "-d", "''", "--output-delimiter=Z"]) .pipe_in("ab\0cd\n") @@ -273,13 +287,38 @@ fn test_equal_as_delimiter3() { } #[test] -fn test_multiple() { - let result = new_ucmd!() +fn test_newline_as_delimiter() { + for (field, expected_output) in [("1", "a:1\n"), ("2", "b:\n")] { + new_ucmd!() + .args(&["-f", field, "-d", "\n"]) + .pipe_in("a:1\nb:") + .succeeds() + .stdout_only_bytes(expected_output); + } +} + +#[test] +fn test_newline_as_delimiter_with_output_delimiter() { + new_ucmd!() + .args(&["-f1-", "-d", "\n", "--output-delimiter=:"]) + .pipe_in("a\nb\n") + .succeeds() + .stdout_only_bytes("a:b\n"); +} + +#[test] +fn test_multiple_delimiters() { + new_ucmd!() .args(&["-f2", "-d:", "-d="]) - .pipe_in("a=b\n") - .succeeds(); - assert_eq!(result.stdout_str(), "b\n"); - assert_eq!(result.stderr_str(), ""); + .pipe_in("a:=b\n") + .succeeds() + .stdout_only("b\n"); + + new_ucmd!() + .args(&["-f2", "-d=", "-d:"]) + .pipe_in("a:=b\n") + .succeeds() + .stdout_only("=b\n"); } #[test] @@ -300,13 +339,6 @@ fn test_multiple_mode_args() { } } -#[test] -fn test_no_argument() { - new_ucmd!().fails().stderr_is( - "cut: invalid usage: expects one of --fields (-f), --chars (-c) or --bytes (-b)\n", - ); -} - #[test] #[cfg(unix)] fn test_8bit_non_utf8_delimiter() { @@ -320,3 +352,29 @@ fn test_8bit_non_utf8_delimiter() { .succeeds() .stdout_check(|out| out == "b_c\n".as_bytes()); } + +#[test] +fn test_newline_preservation_with_f1_option() { + let (at, mut ucmd) = at_and_ucmd!(); + at.write("1", "a\nb"); + let expected = "a\nb\n"; + ucmd.args(&["-f1-", "1"]).succeeds().stdout_is(expected); +} + +#[test] +fn test_output_delimiter_with_character_ranges() { + new_ucmd!() + .args(&["-c2-3,4-", "--output-delim=:"]) + .pipe_in("abcdefg\n") + .succeeds() + .stdout_only("bc:defg\n"); +} + +#[test] +fn test_output_delimiter_with_adjacent_ranges() { + new_ucmd!() + .args(&["-b1-2,3-4", "--output-d=:"]) + .pipe_in("abcd\n") + .succeeds() + .stdout_only("ab:cd\n"); +} diff --git a/tests/by-util/test_dd.rs b/tests/by-util/test_dd.rs index e1e55054a..64ca7603b 100644 --- a/tests/by-util/test_dd.rs +++ b/tests/by-util/test_dd.rs @@ -1658,13 +1658,14 @@ fn test_reading_partial_blocks_from_fifo() { // Start different processes to write to the FIFO, with a small // pause in between. let mut writer_command = Command::new("sh"); - writer_command + let _ = writer_command .args([ "-c", &format!("(printf \"ab\"; sleep 0.1; printf \"cd\") > {fifoname}"), ]) .spawn() - .unwrap(); + .unwrap() + .wait(); let output = child.wait_with_output().unwrap(); assert_eq!(output.stdout, b"abcd"); @@ -1701,13 +1702,14 @@ fn test_reading_partial_blocks_from_fifo_unbuffered() { // Start different processes to write to the FIFO, with a small // pause in between. let mut writer_command = Command::new("sh"); - writer_command + let _ = writer_command .args([ "-c", &format!("(printf \"ab\"; sleep 0.1; printf \"cd\") > {fifoname}"), ]) .spawn() - .unwrap(); + .unwrap() + .wait(); let output = child.wait_with_output().unwrap(); assert_eq!(output.stdout, b"abcd"); diff --git a/tests/by-util/test_du.rs b/tests/by-util/test_du.rs index ef6179e02..ecbf58b11 100644 --- a/tests/by-util/test_du.rs +++ b/tests/by-util/test_du.rs @@ -7,7 +7,7 @@ #[cfg(not(windows))] use regex::Regex; -#[cfg(any(target_os = "linux", target_os = "android"))] +#[cfg(not(target_os = "windows"))] use crate::common::util::expected_result; use crate::common::util::TestScenario; @@ -36,11 +36,11 @@ fn test_du_basics() { return; } } - _du_basics(result.stdout_str()); + du_basics(result.stdout_str()); } #[cfg(target_vendor = "apple")] -fn _du_basics(s: &str) { +fn du_basics(s: &str) { let answer = concat!( "4\t./subdir/deeper/deeper_dir\n", "8\t./subdir/deeper\n", @@ -52,7 +52,7 @@ fn _du_basics(s: &str) { } #[cfg(target_os = "windows")] -fn _du_basics(s: &str) { +fn du_basics(s: &str) { let answer = concat!( "0\t.\\subdir\\deeper\\deeper_dir\n", "0\t.\\subdir\\deeper\n", @@ -64,7 +64,7 @@ fn _du_basics(s: &str) { } #[cfg(all(not(target_vendor = "apple"), not(target_os = "windows"),))] -fn _du_basics(s: &str) { +fn du_basics(s: &str) { let answer = concat!( "8\t./subdir/deeper/deeper_dir\n", "16\t./subdir/deeper\n", @@ -95,19 +95,19 @@ fn test_du_basics_subdir() { return; } } - _du_basics_subdir(result.stdout_str()); + du_basics_subdir(result.stdout_str()); } #[cfg(target_vendor = "apple")] -fn _du_basics_subdir(s: &str) { +fn du_basics_subdir(s: &str) { assert_eq!(s, "4\tsubdir/deeper/deeper_dir\n8\tsubdir/deeper\n"); } #[cfg(target_os = "windows")] -fn _du_basics_subdir(s: &str) { +fn du_basics_subdir(s: &str) { assert_eq!(s, "0\tsubdir/deeper\\deeper_dir\n0\tsubdir/deeper\n"); } #[cfg(target_os = "freebsd")] -fn _du_basics_subdir(s: &str) { +fn du_basics_subdir(s: &str) { assert_eq!(s, "8\tsubdir/deeper/deeper_dir\n16\tsubdir/deeper\n"); } #[cfg(all( @@ -115,7 +115,7 @@ fn _du_basics_subdir(s: &str) { not(target_os = "windows"), not(target_os = "freebsd") ))] -fn _du_basics_subdir(s: &str) { +fn du_basics_subdir(s: &str) { // MS-WSL linux has altered expected output if uucore::os::is_wsl_1() { assert_eq!(s, "0\tsubdir/deeper\n"); @@ -206,20 +206,20 @@ fn test_du_soft_link() { return; } } - _du_soft_link(result.stdout_str()); + du_soft_link(result.stdout_str()); } #[cfg(target_vendor = "apple")] -fn _du_soft_link(s: &str) { +fn du_soft_link(s: &str) { // 'macos' host variants may have `du` output variation for soft links assert!((s == "12\tsubdir/links\n") || (s == "16\tsubdir/links\n")); } #[cfg(target_os = "windows")] -fn _du_soft_link(s: &str) { +fn du_soft_link(s: &str) { assert_eq!(s, "8\tsubdir/links\n"); } #[cfg(target_os = "freebsd")] -fn _du_soft_link(s: &str) { +fn du_soft_link(s: &str) { assert_eq!(s, "16\tsubdir/links\n"); } #[cfg(all( @@ -227,7 +227,7 @@ fn _du_soft_link(s: &str) { not(target_os = "windows"), not(target_os = "freebsd") ))] -fn _du_soft_link(s: &str) { +fn du_soft_link(s: &str) { // MS-WSL linux has altered expected output if uucore::os::is_wsl_1() { assert_eq!(s, "8\tsubdir/links\n"); @@ -255,19 +255,19 @@ fn test_du_hard_link() { } } // We do not double count hard links as the inodes are identical - _du_hard_link(result.stdout_str()); + du_hard_link(result.stdout_str()); } #[cfg(target_vendor = "apple")] -fn _du_hard_link(s: &str) { +fn du_hard_link(s: &str) { assert_eq!(s, "12\tsubdir/links\n"); } #[cfg(target_os = "windows")] -fn _du_hard_link(s: &str) { +fn du_hard_link(s: &str) { assert_eq!(s, "8\tsubdir/links\n"); } #[cfg(target_os = "freebsd")] -fn _du_hard_link(s: &str) { +fn du_hard_link(s: &str) { assert_eq!(s, "16\tsubdir/links\n"); } #[cfg(all( @@ -275,7 +275,7 @@ fn _du_hard_link(s: &str) { not(target_os = "windows"), not(target_os = "freebsd") ))] -fn _du_hard_link(s: &str) { +fn du_hard_link(s: &str) { // MS-WSL linux has altered expected output if uucore::os::is_wsl_1() { assert_eq!(s, "8\tsubdir/links\n"); @@ -299,19 +299,19 @@ fn test_du_d_flag() { return; } } - _du_d_flag(result.stdout_str()); + du_d_flag(result.stdout_str()); } #[cfg(target_vendor = "apple")] -fn _du_d_flag(s: &str) { +fn du_d_flag(s: &str) { assert_eq!(s, "20\t./subdir\n24\t.\n"); } #[cfg(target_os = "windows")] -fn _du_d_flag(s: &str) { +fn du_d_flag(s: &str) { assert_eq!(s, "8\t.\\subdir\n8\t.\n"); } #[cfg(target_os = "freebsd")] -fn _du_d_flag(s: &str) { +fn du_d_flag(s: &str) { assert_eq!(s, "36\t./subdir\n44\t.\n"); } #[cfg(all( @@ -319,7 +319,7 @@ fn _du_d_flag(s: &str) { not(target_os = "windows"), not(target_os = "freebsd") ))] -fn _du_d_flag(s: &str) { +fn du_d_flag(s: &str) { // MS-WSL linux has altered expected output if uucore::os::is_wsl_1() { assert_eq!(s, "8\t./subdir\n8\t.\n"); @@ -348,7 +348,7 @@ fn test_du_dereference() { } } - _du_dereference(result.stdout_str()); + du_dereference(result.stdout_str()); } #[cfg(not(windows))] @@ -376,15 +376,15 @@ fn test_du_dereference_args() { } #[cfg(target_vendor = "apple")] -fn _du_dereference(s: &str) { +fn du_dereference(s: &str) { assert_eq!(s, "4\tsubdir/links/deeper_dir\n16\tsubdir/links\n"); } #[cfg(target_os = "windows")] -fn _du_dereference(s: &str) { +fn du_dereference(s: &str) { assert_eq!(s, "0\tsubdir/links\\deeper_dir\n8\tsubdir/links\n"); } #[cfg(target_os = "freebsd")] -fn _du_dereference(s: &str) { +fn du_dereference(s: &str) { assert_eq!(s, "8\tsubdir/links/deeper_dir\n24\tsubdir/links\n"); } #[cfg(all( @@ -392,7 +392,7 @@ fn _du_dereference(s: &str) { not(target_os = "windows"), not(target_os = "freebsd") ))] -fn _du_dereference(s: &str) { +fn du_dereference(s: &str) { // MS-WSL linux has altered expected output if uucore::os::is_wsl_1() { assert_eq!(s, "0\tsubdir/links/deeper_dir\n8\tsubdir/links\n"); @@ -454,20 +454,15 @@ fn test_du_inodes_basic() { let ts = TestScenario::new(util_name!()); let result = ts.ucmd().arg("--inodes").succeeds(); - #[cfg(any(target_os = "linux", target_os = "android"))] + #[cfg(not(target_os = "windows"))] { let result_reference = unwrap_or_return!(expected_result(&ts, &["--inodes"])); assert_eq!(result.stdout_str(), result_reference.stdout_str()); } - #[cfg(not(any(target_os = "linux", target_os = "android")))] - _du_inodes_basic(result.stdout_str()); -} - -#[cfg(target_os = "windows")] -fn _du_inodes_basic(s: &str) { + #[cfg(target_os = "windows")] assert_eq!( - s, + result.stdout_str(), concat!( "2\t.\\subdir\\deeper\\deeper_dir\n", "4\t.\\subdir\\deeper\n", @@ -478,20 +473,6 @@ fn _du_inodes_basic(s: &str) { ); } -#[cfg(not(target_os = "windows"))] -fn _du_inodes_basic(s: &str) { - assert_eq!( - s, - concat!( - "2\t./subdir/deeper/deeper_dir\n", - "4\t./subdir/deeper\n", - "3\t./subdir/links\n", - "8\t./subdir\n", - "11\t.\n", - ) - ); -} - #[test] fn test_du_inodes() { let ts = TestScenario::new(util_name!()); @@ -546,6 +527,33 @@ fn test_du_inodes_with_count_links() { } } +#[cfg(not(target_os = "android"))] +#[test] +fn test_du_inodes_with_count_links_all() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + + at.mkdir("d"); + at.mkdir("d/d"); + at.touch("d/f"); + at.hard_link("d/f", "d/h"); + + let result = ts.ucmd().arg("--inodes").arg("-al").arg("d").succeeds(); + result.no_stderr(); + + let mut result_seq: Vec = result + .stdout_str() + .split('\n') + .filter(|x| !x.is_empty()) + .map(|x| x.parse().unwrap()) + .collect(); + result_seq.sort_unstable(); + #[cfg(windows)] + assert_eq!(result_seq, ["1\td\\d", "1\td\\f", "1\td\\h", "4\td"]); + #[cfg(not(windows))] + assert_eq!(result_seq, ["1\td/d", "1\td/f", "1\td/h", "4\td"]); +} + #[test] fn test_du_h_flag_empty_file() { new_ucmd!() @@ -679,8 +687,10 @@ fn test_du_no_permission() { return; } } - - _du_no_permission(result.stdout_str()); + #[cfg(not(target_vendor = "apple"))] + assert_eq!(result.stdout_str(), "4\tsubdir/links\n"); + #[cfg(target_vendor = "apple")] + assert_eq!(result.stdout_str(), "0\tsubdir/links\n"); } #[cfg(not(target_os = "windows"))] @@ -698,15 +708,6 @@ fn test_du_no_exec_permission() { result.stderr_contains("du: cannot access 'd/no-x/y': Permission denied"); } -#[cfg(target_vendor = "apple")] -fn _du_no_permission(s: &str) { - assert_eq!(s, "0\tsubdir/links\n"); -} -#[cfg(all(not(target_vendor = "apple"), not(target_os = "windows")))] -fn _du_no_permission(s: &str) { - assert_eq!(s, "4\tsubdir/links\n"); -} - #[test] #[cfg(not(target_os = "openbsd"))] fn test_du_one_file_system() { @@ -722,7 +723,7 @@ fn test_du_one_file_system() { return; } } - _du_basics_subdir(result.stdout_str()); + du_basics_subdir(result.stdout_str()); } #[test] @@ -1171,3 +1172,83 @@ fn test_invalid_time_style() { .succeeds() .stdout_does_not_contain("du: invalid argument 'banana' for 'time style'"); } + +#[test] +fn test_human_size() { + use std::fs::File; + + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + let dir = at.plus_as_string("d"); + at.mkdir(&dir); + + for i in 1..=1023 { + let file_path = format!("{dir}/file{i}"); + File::create(&file_path).expect("Failed to create file"); + } + + ts.ucmd() + .arg("--inodes") + .arg("-h") + .arg(&dir) + .succeeds() + .stdout_contains(format!("1.0K {dir}")); +} + +#[cfg(not(target_os = "android"))] +#[test] +fn test_du_deduplicated_input_args() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + + at.mkdir("d"); + at.mkdir("d/d"); + at.touch("d/f"); + at.hard_link("d/f", "d/h"); + + let result = ts + .ucmd() + .arg("--inodes") + .arg("d") + .arg("d") + .arg("d") + .succeeds(); + result.no_stderr(); + + let result_seq: Vec = result + .stdout_str() + .lines() + .map(|x| x.parse().unwrap()) + .collect(); + #[cfg(windows)] + assert_eq!(result_seq, ["1\td\\d", "3\td"]); + #[cfg(not(windows))] + assert_eq!(result_seq, ["1\td/d", "3\td"]); +} + +#[cfg(not(target_os = "android"))] +#[test] +fn test_du_no_deduplicated_input_args() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + + at.mkdir("d"); + at.touch("d/d"); + + let result = ts + .ucmd() + .arg("--inodes") + .arg("-l") + .arg("d") + .arg("d") + .arg("d") + .succeeds(); + result.no_stderr(); + + let result_seq: Vec = result + .stdout_str() + .lines() + .map(|x| x.parse().unwrap()) + .collect(); + assert_eq!(result_seq, ["2\td", "2\td", "2\td"]); +} diff --git a/tests/by-util/test_echo.rs b/tests/by-util/test_echo.rs index 136500b48..dd6b412a4 100644 --- a/tests/by-util/test_echo.rs +++ b/tests/by-util/test_echo.rs @@ -219,8 +219,7 @@ fn test_hyphen_values_at_start() { .arg("-test") .arg("araba") .arg("-merci") - .run() - .success() + .succeeds() .stdout_does_not_contain("-E") .stdout_is("-test araba -merci\n"); } @@ -231,8 +230,7 @@ fn test_hyphen_values_between() { .arg("test") .arg("-E") .arg("araba") - .run() - .success() + .succeeds() .stdout_is("test -E araba\n"); new_ucmd!() @@ -240,11 +238,20 @@ fn test_hyphen_values_between() { .arg("dum dum dum") .arg("-e") .arg("dum") - .run() - .success() + .succeeds() .stdout_is("dumdum dum dum dum -e dum\n"); } +#[test] +fn test_double_hyphens() { + new_ucmd!().arg("--").succeeds().stdout_only("--\n"); + new_ucmd!() + .arg("--") + .arg("--") + .succeeds() + .stdout_only("-- --\n"); +} + #[test] fn wrapping_octal() { // Some odd behavior of GNU. Values of \0400 and greater do not fit in the diff --git a/tests/by-util/test_env.rs b/tests/by-util/test_env.rs index 208feab6d..a1b13e020 100644 --- a/tests/by-util/test_env.rs +++ b/tests/by-util/test_env.rs @@ -10,6 +10,7 @@ use crate::common::util::TestScenario; use crate::common::util::UChild; #[cfg(unix)] use nix::sys::signal::Signal; +#[cfg(feature = "echo")] use regex::Regex; use std::env; use std::path::Path; @@ -35,13 +36,14 @@ impl Target { Self { child } } fn send_signal(&mut self, signal: Signal) { - Command::new("kill") + let _ = Command::new("kill") .args(&[ format!("-{}", signal as i32), format!("{}", self.child.id()), ]) .spawn() - .expect("failed to send signal"); + .expect("failed to send signal") + .wait(); self.child.delay(100); } fn is_alive(&mut self) -> bool { @@ -98,6 +100,7 @@ fn test_if_windows_batch_files_can_be_executed() { assert!(result.stdout_str().contains("Hello Windows World!")); } +#[cfg(feature = "echo")] #[test] fn test_debug_1() { let ts = TestScenario::new(util_name!()); @@ -118,6 +121,7 @@ fn test_debug_1() { ); } +#[cfg(feature = "echo")] #[test] fn test_debug_2() { let ts = TestScenario::new(util_name!()); @@ -144,6 +148,7 @@ fn test_debug_2() { ); } +#[cfg(feature = "echo")] #[test] fn test_debug1_part_of_string_arg() { let ts = TestScenario::new(util_name!()); @@ -165,6 +170,7 @@ fn test_debug1_part_of_string_arg() { ); } +#[cfg(feature = "echo")] #[test] fn test_debug2_part_of_string_arg() { let ts = TestScenario::new(util_name!()); @@ -651,7 +657,7 @@ fn test_env_with_empty_executable_double_quotes() { } #[test] -#[cfg(unix)] +#[cfg(all(unix, feature = "dirname", feature = "echo"))] fn test_env_overwrite_arg0() { let ts = TestScenario::new(util_name!()); @@ -675,7 +681,7 @@ fn test_env_overwrite_arg0() { } #[test] -#[cfg(unix)] +#[cfg(all(unix, feature = "echo"))] fn test_env_arg_argv0_overwrite() { let ts = TestScenario::new(util_name!()); @@ -723,7 +729,7 @@ fn test_env_arg_argv0_overwrite() { } #[test] -#[cfg(unix)] +#[cfg(all(unix, feature = "echo"))] fn test_env_arg_argv0_overwrite_mixed_with_string_args() { let ts = TestScenario::new(util_name!()); diff --git a/tests/by-util/test_fmt.rs b/tests/by-util/test_fmt.rs index 9bb82ede5..fb6416430 100644 --- a/tests/by-util/test_fmt.rs +++ b/tests/by-util/test_fmt.rs @@ -9,6 +9,11 @@ fn test_invalid_arg() { new_ucmd!().arg("--definitely-invalid").fails().code_is(1); } +#[test] +fn test_invalid_input() { + new_ucmd!().arg(".").fails().code_is(1); +} + #[test] fn test_fmt() { new_ucmd!() diff --git a/tests/by-util/test_ls.rs b/tests/by-util/test_ls.rs index 0c0d8e3a8..f65078a0d 100644 --- a/tests/by-util/test_ls.rs +++ b/tests/by-util/test_ls.rs @@ -3,6 +3,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. // spell-checker:ignore (words) READMECAREFULLY birthtime doesntexist oneline somebackup lrwx somefile somegroup somehiddenbackup somehiddenfile tabsize aaaaaaaa bbbb cccc dddddddd ncccc neee naaaaa nbcdef nfffff dired subdired tmpfs mdir COLORTERM mexe bcdef mfoo +// spell-checker:ignore (words) fakeroot setcap #![allow( clippy::similar_names, clippy::too_many_lines, @@ -1329,10 +1330,10 @@ fn test_ls_long_symlink_color() { Some(captures) => { dbg!(captures.get(1).unwrap().as_str().to_string()); dbg!(captures.get(2).unwrap().as_str().to_string()); - return ( + ( captures.get(1).unwrap().as_str().to_string(), captures.get(2).unwrap().as_str().to_string(), - ); + ) } None => (String::new(), input.to_string()), } @@ -5516,3 +5517,49 @@ fn test_suffix_case_sensitivity() { /* cSpell:enable */ ); } + +#[cfg(all(unix, target_os = "linux"))] +#[test] +fn test_ls_capabilities() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + // Test must be run as root (or with `sudo -E`) + // fakeroot setcap cap_net_bind_service=ep /tmp/file_name + // doesn't trigger an error and fails silently + if scene.cmd("whoami").run().stdout_str() != "root\n" { + return; + } + at.mkdir("test"); + at.mkdir("test/dir"); + at.touch("test/cap_pos"); + at.touch("test/dir/cap_neg"); + at.touch("test/dir/cap_pos"); + + let files = ["test/cap_pos", "test/dir/cap_pos"]; + for file in &files { + scene + .cmd("sudo") + .args(&[ + "-E", + "--non-interactive", + "setcap", + "cap_net_bind_service=ep", + at.plus(file).to_str().unwrap(), + ]) + .succeeds(); + } + + let ls_colors = "di=:ca=30;41"; + + scene + .ucmd() + .env("LS_COLORS", ls_colors) + .arg("--color=always") + .arg("test/cap_pos") + .arg("test/dir") + .succeeds() + .stdout_contains("\x1b[30;41mtest/cap_pos") // spell-checker:disable-line + .stdout_contains("\x1b[30;41mcap_pos") // spell-checker:disable-line + .stdout_does_not_contain("0;41mtest/dir/cap_neg"); // spell-checker:disable-line +} diff --git a/tests/by-util/test_mkfifo.rs b/tests/by-util/test_mkfifo.rs index 731b6c1d5..e25bbfc44 100644 --- a/tests/by-util/test_mkfifo.rs +++ b/tests/by-util/test_mkfifo.rs @@ -52,3 +52,50 @@ fn test_create_one_fifo_already_exists() { .fails() .stderr_is("mkfifo: cannot create fifo 'abcdef': File exists\n"); } + +#[test] +fn test_create_fifo_with_mode_and_umask() { + use uucore::fs::display_permissions; + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let test_fifo_creation = |mode: &str, umask: u16, expected: &str| { + scene + .ucmd() + .arg("-m") + .arg(mode) + .arg(format!("fifo_test_{mode}")) + .umask(libc::mode_t::from(umask)) + .succeeds(); + + let metadata = std::fs::metadata(at.subdir.join(format!("fifo_test_{mode}"))).unwrap(); + let permissions = display_permissions(&metadata, true); + assert_eq!(permissions, expected.to_string()); + }; + + test_fifo_creation("734", 0o077, "prwx-wxr--"); // spell-checker:disable-line + test_fifo_creation("706", 0o777, "prwx---rw-"); // spell-checker:disable-line +} + +#[test] +fn test_create_fifo_with_umask() { + use uucore::fs::display_permissions; + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let test_fifo_creation = |umask: u16, expected: &str| { + scene + .ucmd() + .arg("fifo_test") + .umask(libc::mode_t::from(umask)) + .succeeds(); + + let metadata = std::fs::metadata(at.subdir.join("fifo_test")).unwrap(); + let permissions = display_permissions(&metadata, true); + assert_eq!(permissions, expected.to_string()); + at.remove("fifo_test"); + }; + + test_fifo_creation(0o022, "prw-r--r--"); // spell-checker:disable-line + test_fifo_creation(0o777, "p---------"); // spell-checker:disable-line +} diff --git a/tests/by-util/test_mv.rs b/tests/by-util/test_mv.rs index d8bc49e8e..1419be4e9 100644 --- a/tests/by-util/test_mv.rs +++ b/tests/by-util/test_mv.rs @@ -6,8 +6,8 @@ // spell-checker:ignore mydir use crate::common::util::TestScenario; use filetime::FileTime; -use std::thread::sleep; -use std::time::Duration; +use rstest::rstest; +use std::io::Write; #[test] fn test_mv_invalid_arg() { @@ -468,7 +468,31 @@ fn test_mv_same_symlink() { .arg(file_c) .arg(file_a) .fails() - .stderr_is(format!("mv: '{file_c}' and '{file_a}' are the same file\n",)); + .stderr_is(format!("mv: '{file_c}' and '{file_a}' are the same file\n")); +} + +#[test] +#[cfg(all(unix, not(target_os = "android")))] +fn test_mv_same_broken_symlink() { + let (at, mut ucmd) = at_and_ucmd!(); + + at.symlink_file("missing-target", "broken"); + + ucmd.arg("broken") + .arg("broken") + .fails() + .stderr_is("mv: 'broken' and 'broken' are the same file\n"); +} + +#[test] +#[cfg(all(unix, not(target_os = "android")))] +fn test_mv_symlink_into_target() { + let (at, mut ucmd) = at_and_ucmd!(); + + at.mkdir("dir"); + at.symlink_file("dir", "dir-link"); + + ucmd.arg("dir-link").arg("dir").succeeds(); } #[test] @@ -572,6 +596,30 @@ fn test_mv_simple_backup() { assert!(at.file_exists(format!("{file_b}~"))); } +#[test] +fn test_mv_simple_backup_for_directory() { + let (at, mut ucmd) = at_and_ucmd!(); + let dir_a = "test_mv_simple_backup_dir_a"; + let dir_b = "test_mv_simple_backup_dir_b"; + + at.mkdir(dir_a); + at.mkdir(dir_b); + at.touch(format!("{dir_a}/file_a")); + at.touch(format!("{dir_b}/file_b")); + ucmd.arg("-T") + .arg("-b") + .arg(dir_a) + .arg(dir_b) + .succeeds() + .no_stderr(); + + assert!(!at.dir_exists(dir_a)); + assert!(at.dir_exists(dir_b)); + assert!(at.dir_exists(&format!("{dir_b}~"))); + assert!(at.file_exists(format!("{dir_b}/file_a"))); + assert!(at.file_exists(format!("{dir_b}~/file_b"))); +} + #[test] fn test_mv_simple_backup_with_file_extension() { let (at, mut ucmd) = at_and_ucmd!(); @@ -974,9 +1022,9 @@ fn test_mv_arg_update_older_dest_not_older() { let old_content = "file1 content\n"; let new_content = "file2 content\n"; - at.write(old, old_content); - - sleep(Duration::from_secs(1)); + let mut f = at.make_file(old); + f.write_all(old_content.as_bytes()).unwrap(); + f.set_modified(std::time::UNIX_EPOCH).unwrap(); at.write(new, new_content); @@ -1001,9 +1049,9 @@ fn test_mv_arg_update_none_then_all() { let old_content = "old content\n"; let new_content = "new content\n"; - at.write(old, old_content); - - sleep(Duration::from_secs(1)); + let mut f = at.make_file(old); + f.write_all(old_content.as_bytes()).unwrap(); + f.set_modified(std::time::UNIX_EPOCH).unwrap(); at.write(new, new_content); @@ -1029,9 +1077,9 @@ fn test_mv_arg_update_all_then_none() { let old_content = "old content\n"; let new_content = "new content\n"; - at.write(old, old_content); - - sleep(Duration::from_secs(1)); + let mut f = at.make_file(old); + f.write_all(old_content.as_bytes()).unwrap(); + f.set_modified(std::time::UNIX_EPOCH).unwrap(); at.write(new, new_content); @@ -1055,9 +1103,9 @@ fn test_mv_arg_update_older_dest_older() { let old_content = "file1 content\n"; let new_content = "file2 content\n"; - at.write(old, old_content); - - sleep(Duration::from_secs(1)); + let mut f = at.make_file(old); + f.write_all(old_content.as_bytes()).unwrap(); + f.set_modified(std::time::UNIX_EPOCH).unwrap(); at.write(new, new_content); @@ -1081,9 +1129,9 @@ fn test_mv_arg_update_short_overwrite() { let old_content = "file1 content\n"; let new_content = "file2 content\n"; - at.write(old, old_content); - - sleep(Duration::from_secs(1)); + let mut f = at.make_file(old); + f.write_all(old_content.as_bytes()).unwrap(); + f.set_modified(std::time::UNIX_EPOCH).unwrap(); at.write(new, new_content); @@ -1107,9 +1155,9 @@ fn test_mv_arg_update_short_no_overwrite() { let old_content = "file1 content\n"; let new_content = "file2 content\n"; - at.write(old, old_content); - - sleep(Duration::from_secs(1)); + let mut f = at.make_file(old); + f.write_all(old_content.as_bytes()).unwrap(); + f.set_modified(std::time::UNIX_EPOCH).unwrap(); at.write(new, new_content); @@ -1366,24 +1414,6 @@ fn test_mv_interactive_error() { .is_empty()); } -#[test] -fn test_mv_into_self() { - let scene = TestScenario::new(util_name!()); - let at = &scene.fixtures; - let dir1 = "dir1"; - let dir2 = "dir2"; - at.mkdir(dir1); - at.mkdir(dir2); - - scene - .ucmd() - .arg(dir1) - .arg(dir2) - .arg(dir2) - .fails() - .stderr_contains("mv: cannot move 'dir2' to a subdirectory of itself, 'dir2/dir2'"); -} - #[test] fn test_mv_arg_interactive_skipped() { let (at, mut ucmd) = at_and_ucmd!(); @@ -1433,27 +1463,32 @@ fn test_mv_into_self_data() { assert!(!at.file_exists(file1)); } -#[test] -fn test_mv_directory_into_subdirectory_of_itself_fails() { +#[rstest] +#[case(vec!["mydir"], vec!["mydir", "mydir"], "mv: cannot move 'mydir' to a subdirectory of itself, 'mydir/mydir'")] +#[case(vec!["mydir"], vec!["mydir/", "mydir/"], "mv: cannot move 'mydir/' to a subdirectory of itself, 'mydir/mydir'")] +#[case(vec!["mydir"], vec!["./mydir", "mydir", "mydir/"], "mv: cannot move './mydir' to a subdirectory of itself, 'mydir/mydir'")] +#[case(vec!["mydir"], vec!["mydir/", "mydir/mydir_2/"], "mv: cannot move 'mydir/' to a subdirectory of itself, 'mydir/mydir_2/'")] +#[case(vec!["mydir/mydir_2"], vec!["mydir", "mydir/mydir_2"], "mv: cannot move 'mydir' to a subdirectory of itself, 'mydir/mydir_2/mydir'\n")] +#[case(vec!["mydir/mydir_2"], vec!["mydir/", "mydir/mydir_2/"], "mv: cannot move 'mydir/' to a subdirectory of itself, 'mydir/mydir_2/mydir'\n")] +#[case(vec!["mydir", "mydir_2"], vec!["mydir/", "mydir_2/", "mydir_2/"], "mv: cannot move 'mydir_2/' to a subdirectory of itself, 'mydir_2/mydir_2'")] +#[case(vec!["mydir"], vec!["mydir/", "mydir"], "mv: cannot move 'mydir/' to a subdirectory of itself, 'mydir/mydir'")] +#[case(vec!["mydir"], vec!["-T", "mydir", "mydir"], "mv: 'mydir' and 'mydir' are the same file")] +#[case(vec!["mydir"], vec!["mydir/", "mydir/../"], "mv: 'mydir/' and 'mydir/../mydir' are the same file")] +fn test_mv_directory_self( + #[case] dirs: Vec<&str>, + #[case] args: Vec<&str>, + #[case] expected_error: &str, +) { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; - let dir1 = "mydir"; - let dir2 = "mydir/mydir_2"; - at.mkdir(dir1); - at.mkdir(dir2); - scene.ucmd().arg(dir1).arg(dir2).fails().stderr_contains( - "mv: cannot move 'mydir' to a subdirectory of itself, 'mydir/mydir_2/mydir'", - ); - - // check that it also errors out with / + for dir in dirs { + at.mkdir_all(dir); + } scene .ucmd() - .arg(format!("{dir1}/")) - .arg(dir2) + .args(&args) .fails() - .stderr_contains( - "mv: cannot move 'mydir/' to a subdirectory of itself, 'mydir/mydir_2/mydir/'", - ); + .stderr_contains(expected_error); } #[test] diff --git a/tests/by-util/test_rm.rs b/tests/by-util/test_rm.rs index f997688c8..b220926fe 100644 --- a/tests/by-util/test_rm.rs +++ b/tests/by-util/test_rm.rs @@ -677,6 +677,68 @@ fn test_remove_inaccessible_dir() { assert!(!at.dir_exists(dir_1)); } +#[test] +#[cfg(not(windows))] +fn test_rm_current_or_parent_dir_rm4() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + + at.mkdir("d"); + + let answers = [ + "rm: refusing to remove '.' or '..' directory: skipping 'd/.'", + "rm: refusing to remove '.' or '..' directory: skipping 'd/./'", + "rm: refusing to remove '.' or '..' directory: skipping 'd/./'", + "rm: refusing to remove '.' or '..' directory: skipping 'd/..'", + "rm: refusing to remove '.' or '..' directory: skipping 'd/../'", + ]; + let std_err_str = ts + .ucmd() + .arg("-rf") + .arg("d/.") + .arg("d/./") + .arg("d/.////") + .arg("d/..") + .arg("d/../") + .fails() + .stderr_move_str(); + + for (idx, line) in std_err_str.lines().enumerate() { + assert_eq!(line, answers[idx]); + } +} + +#[test] +#[cfg(windows)] +fn test_rm_current_or_parent_dir_rm4_windows() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + + at.mkdir("d"); + + let answers = [ + "rm: refusing to remove '.' or '..' directory: skipping 'd\\.'", + "rm: refusing to remove '.' or '..' directory: skipping 'd\\.\\'", + "rm: refusing to remove '.' or '..' directory: skipping 'd\\.\\'", + "rm: refusing to remove '.' or '..' directory: skipping 'd\\..'", + "rm: refusing to remove '.' or '..' directory: skipping 'd\\..\\'", + ]; + let std_err_str = ts + .ucmd() + .arg("-rf") + .arg("d\\.") + .arg("d\\.\\") + .arg("d\\.\\\\\\\\") + .arg("d\\..") + .arg("d\\..\\") + .fails() + .stderr_move_str(); + + for (idx, line) in std_err_str.lines().enumerate() { + assert_eq!(line, answers[idx]); + } +} + #[test] #[cfg(not(windows))] fn test_fifo_removal() { diff --git a/tests/by-util/test_seq.rs b/tests/by-util/test_seq.rs index a8bd1fb83..96460cf5f 100644 --- a/tests/by-util/test_seq.rs +++ b/tests/by-util/test_seq.rs @@ -48,12 +48,12 @@ fn test_hex_rejects_sign_after_identifier() { .args(&["-0x-123ABC"]) .fails() .no_stdout() - .stderr_contains("unexpected argument '-0' found"); + .usage_error("invalid floating point argument: '-0x-123ABC'"); new_ucmd!() .args(&["-0x+123ABC"]) .fails() .no_stdout() - .stderr_contains("unexpected argument '-0' found"); + .usage_error("invalid floating point argument: '-0x+123ABC'"); } #[test] @@ -303,18 +303,15 @@ fn test_preserve_negative_zero_start() { new_ucmd!() .args(&["-0", "1"]) .succeeds() - .stdout_is("-0\n1\n") - .no_stderr(); + .stdout_only("-0\n1\n"); new_ucmd!() .args(&["-0", "1", "2"]) .succeeds() - .stdout_is("-0\n1\n2\n") - .no_stderr(); + .stdout_only("-0\n1\n2\n"); new_ucmd!() .args(&["-0", "1", "2.0"]) .succeeds() - .stdout_is("-0\n1\n2\n") - .no_stderr(); + .stdout_only("-0\n1\n2\n"); } #[test] @@ -322,8 +319,7 @@ fn test_drop_negative_zero_end() { new_ucmd!() .args(&["1", "-1", "-0"]) .succeeds() - .stdout_is("1\n0\n") - .no_stderr(); + .stdout_only("1\n0\n"); } #[test] @@ -331,8 +327,11 @@ fn test_width_scientific_notation() { new_ucmd!() .args(&["-w", "999", "1e3"]) .succeeds() - .stdout_is("0999\n1000\n") - .no_stderr(); + .stdout_only("0999\n1000\n"); + new_ucmd!() + .args(&["-w", "999", "1E3"]) + .succeeds() + .stdout_only("0999\n1000\n"); } #[test] @@ -340,18 +339,15 @@ fn test_width_negative_zero() { new_ucmd!() .args(&["-w", "-0", "1"]) .succeeds() - .stdout_is("-0\n01\n") - .no_stderr(); + .stdout_only("-0\n01\n"); new_ucmd!() .args(&["-w", "-0", "1", "2"]) .succeeds() - .stdout_is("-0\n01\n02\n") - .no_stderr(); + .stdout_only("-0\n01\n02\n"); new_ucmd!() .args(&["-w", "-0", "1", "2.0"]) .succeeds() - .stdout_is("-0\n01\n02\n") - .no_stderr(); + .stdout_only("-0\n01\n02\n"); } #[test] @@ -359,33 +355,27 @@ fn test_width_negative_zero_decimal_notation() { new_ucmd!() .args(&["-w", "-0.0", "1"]) .succeeds() - .stdout_is("-0.0\n01.0\n") - .no_stderr(); + .stdout_only("-0.0\n01.0\n"); new_ucmd!() .args(&["-w", "-0.0", "1.0"]) .succeeds() - .stdout_is("-0.0\n01.0\n") - .no_stderr(); + .stdout_only("-0.0\n01.0\n"); new_ucmd!() .args(&["-w", "-0.0", "1", "2"]) .succeeds() - .stdout_is("-0.0\n01.0\n02.0\n") - .no_stderr(); + .stdout_only("-0.0\n01.0\n02.0\n"); new_ucmd!() .args(&["-w", "-0.0", "1", "2.0"]) .succeeds() - .stdout_is("-0.0\n01.0\n02.0\n") - .no_stderr(); + .stdout_only("-0.0\n01.0\n02.0\n"); new_ucmd!() .args(&["-w", "-0.0", "1.0", "2"]) .succeeds() - .stdout_is("-0.0\n01.0\n02.0\n") - .no_stderr(); + .stdout_only("-0.0\n01.0\n02.0\n"); new_ucmd!() .args(&["-w", "-0.0", "1.0", "2.0"]) .succeeds() - .stdout_is("-0.0\n01.0\n02.0\n") - .no_stderr(); + .stdout_only("-0.0\n01.0\n02.0\n"); } #[test] @@ -393,98 +383,80 @@ fn test_width_negative_zero_scientific_notation() { new_ucmd!() .args(&["-w", "-0e0", "1"]) .succeeds() - .stdout_is("-0\n01\n") - .no_stderr(); + .stdout_only("-0\n01\n"); new_ucmd!() .args(&["-w", "-0e0", "1", "2"]) .succeeds() - .stdout_is("-0\n01\n02\n") - .no_stderr(); + .stdout_only("-0\n01\n02\n"); new_ucmd!() .args(&["-w", "-0e0", "1", "2.0"]) .succeeds() - .stdout_is("-0\n01\n02\n") - .no_stderr(); + .stdout_only("-0\n01\n02\n"); new_ucmd!() .args(&["-w", "-0e+1", "1"]) .succeeds() - .stdout_is("-00\n001\n") - .no_stderr(); + .stdout_only("-00\n001\n"); new_ucmd!() .args(&["-w", "-0e+1", "1", "2"]) .succeeds() - .stdout_is("-00\n001\n002\n") - .no_stderr(); + .stdout_only("-00\n001\n002\n"); new_ucmd!() .args(&["-w", "-0e+1", "1", "2.0"]) .succeeds() - .stdout_is("-00\n001\n002\n") - .no_stderr(); + .stdout_only("-00\n001\n002\n"); new_ucmd!() .args(&["-w", "-0.000e0", "1"]) .succeeds() - .stdout_is("-0.000\n01.000\n") - .no_stderr(); + .stdout_only("-0.000\n01.000\n"); new_ucmd!() .args(&["-w", "-0.000e0", "1", "2"]) .succeeds() - .stdout_is("-0.000\n01.000\n02.000\n") - .no_stderr(); + .stdout_only("-0.000\n01.000\n02.000\n"); new_ucmd!() .args(&["-w", "-0.000e0", "1", "2.0"]) .succeeds() - .stdout_is("-0.000\n01.000\n02.000\n") - .no_stderr(); + .stdout_only("-0.000\n01.000\n02.000\n"); new_ucmd!() .args(&["-w", "-0.000e-2", "1"]) .succeeds() - .stdout_is("-0.00000\n01.00000\n") - .no_stderr(); + .stdout_only("-0.00000\n01.00000\n"); new_ucmd!() .args(&["-w", "-0.000e-2", "1", "2"]) .succeeds() - .stdout_is("-0.00000\n01.00000\n02.00000\n") - .no_stderr(); + .stdout_only("-0.00000\n01.00000\n02.00000\n"); new_ucmd!() .args(&["-w", "-0.000e-2", "1", "2.0"]) .succeeds() - .stdout_is("-0.00000\n01.00000\n02.00000\n") - .no_stderr(); + .stdout_only("-0.00000\n01.00000\n02.00000\n"); new_ucmd!() .args(&["-w", "-0.000e5", "1"]) .succeeds() - .stdout_is("-000000\n0000001\n") - .no_stderr(); + .stdout_only("-000000\n0000001\n"); new_ucmd!() .args(&["-w", "-0.000e5", "1", "2"]) .succeeds() - .stdout_is("-000000\n0000001\n0000002\n") - .no_stderr(); + .stdout_only("-000000\n0000001\n0000002\n"); new_ucmd!() .args(&["-w", "-0.000e5", "1", "2.0"]) .succeeds() - .stdout_is("-000000\n0000001\n0000002\n") - .no_stderr(); + .stdout_only("-000000\n0000001\n0000002\n"); new_ucmd!() .args(&["-w", "-0.000e5", "1"]) .succeeds() - .stdout_is("-000000\n0000001\n") - .no_stderr(); + .stdout_only("-000000\n0000001\n"); new_ucmd!() .args(&["-w", "-0.000e5", "1", "2"]) .succeeds() - .stdout_is("-000000\n0000001\n0000002\n") - .no_stderr(); + .stdout_only("-000000\n0000001\n0000002\n"); new_ucmd!() .args(&["-w", "-0.000e5", "1", "2.0"]) .succeeds() - .stdout_is("-000000\n0000001\n0000002\n") - .no_stderr(); + .stdout_only("-000000\n0000001\n0000002\n"); } #[test] @@ -492,14 +464,12 @@ fn test_width_decimal_scientific_notation_increment() { new_ucmd!() .args(&["-w", ".1", "1e-2", ".11"]) .succeeds() - .stdout_is("0.10\n0.11\n") - .no_stderr(); + .stdout_only("0.10\n0.11\n"); new_ucmd!() .args(&["-w", ".0", "1.500e-1", ".2"]) .succeeds() - .stdout_is("0.0000\n0.1500\n") - .no_stderr(); + .stdout_only("0.0000\n0.1500\n"); } /// Test that trailing zeros in the start argument contribute to precision. @@ -508,8 +478,7 @@ fn test_width_decimal_scientific_notation_trailing_zeros_start() { new_ucmd!() .args(&["-w", ".1000", "1e-2", ".11"]) .succeeds() - .stdout_is("0.1000\n0.1100\n") - .no_stderr(); + .stdout_only("0.1000\n0.1100\n"); } /// Test that trailing zeros in the increment argument contribute to precision. @@ -518,8 +487,7 @@ fn test_width_decimal_scientific_notation_trailing_zeros_increment() { new_ucmd!() .args(&["-w", "1e-1", "0.0100", ".11"]) .succeeds() - .stdout_is("0.1000\n0.1100\n") - .no_stderr(); + .stdout_only("0.1000\n0.1100\n"); } #[test] @@ -527,8 +495,7 @@ fn test_width_negative_decimal_notation() { new_ucmd!() .args(&["-w", "-.1", ".1", ".11"]) .succeeds() - .stdout_is("-0.1\n00.0\n00.1\n") - .no_stderr(); + .stdout_only("-0.1\n00.0\n00.1\n"); } #[test] @@ -536,22 +503,19 @@ fn test_width_negative_scientific_notation() { new_ucmd!() .args(&["-w", "-1e-3", "1"]) .succeeds() - .stdout_is("-0.001\n00.999\n") - .no_stderr(); + .stdout_only("-0.001\n00.999\n"); new_ucmd!() .args(&["-w", "-1.e-3", "1"]) .succeeds() - .stdout_is("-0.001\n00.999\n") - .no_stderr(); + .stdout_only("-0.001\n00.999\n"); new_ucmd!() .args(&["-w", "-1.0e-4", "1"]) .succeeds() - .stdout_is("-0.00010\n00.99990\n") - .no_stderr(); + .stdout_only("-0.00010\n00.99990\n"); new_ucmd!() .args(&["-w", "-.1e2", "10", "100"]) .succeeds() - .stdout_is( + .stdout_only( "-010 0000 0010 @@ -565,12 +529,11 @@ fn test_width_negative_scientific_notation() { 0090 0100 ", - ) - .no_stderr(); + ); new_ucmd!() .args(&["-w", "-0.1e2", "10", "100"]) .succeeds() - .stdout_is( + .stdout_only( "-010 0000 0010 @@ -584,8 +547,7 @@ fn test_width_negative_scientific_notation() { 0090 0100 ", - ) - .no_stderr(); + ); } /// Test that trailing zeros in the end argument do not contribute to width. @@ -594,8 +556,7 @@ fn test_width_decimal_scientific_notation_trailing_zeros_end() { new_ucmd!() .args(&["-w", "1e-1", "1e-2", ".1100"]) .succeeds() - .stdout_is("0.10\n0.11\n") - .no_stderr(); + .stdout_only("0.10\n0.11\n"); } #[test] @@ -603,8 +564,7 @@ fn test_width_floats() { new_ucmd!() .args(&["-w", "9.0", "10.0"]) .succeeds() - .stdout_is("09.0\n10.0\n") - .no_stderr(); + .stdout_only("09.0\n10.0\n"); } // TODO This is duplicated from `test_yes.rs`; refactor them. @@ -656,11 +616,7 @@ fn test_neg_inf_width() { #[test] fn test_ignore_leading_whitespace() { - new_ucmd!() - .arg(" 1") - .succeeds() - .stdout_is("1\n") - .no_stderr(); + new_ucmd!().arg(" 1").succeeds().stdout_only("1\n"); } #[test] @@ -679,8 +635,7 @@ fn test_negative_zero_int_start_float_increment() { new_ucmd!() .args(&["-0", "0.1", "0.1"]) .succeeds() - .stdout_is("-0.0\n0.1\n") - .no_stderr(); + .stdout_only("-0.0\n0.1\n"); } #[test] @@ -688,7 +643,7 @@ fn test_float_precision_increment() { new_ucmd!() .args(&["999", "0.1", "1000.1"]) .succeeds() - .stdout_is( + .stdout_only( "999.0 999.1 999.2 @@ -702,8 +657,7 @@ fn test_float_precision_increment() { 1000.0 1000.1 ", - ) - .no_stderr(); + ); } /// Test for floating point precision issues. @@ -712,8 +666,7 @@ fn test_negative_increment_decimal() { new_ucmd!() .args(&["0.1", "-0.1", "-0.2"]) .succeeds() - .stdout_is("0.1\n0.0\n-0.1\n-0.2\n") - .no_stderr(); + .stdout_only("0.1\n0.0\n-0.1\n-0.2\n"); } #[test] @@ -721,8 +674,7 @@ fn test_zero_not_first() { new_ucmd!() .args(&["-w", "-0.1", "0.1", "0.1"]) .succeeds() - .stdout_is("-0.1\n00.0\n00.1\n") - .no_stderr(); + .stdout_only("-0.1\n00.0\n00.1\n"); } #[test] @@ -730,8 +682,7 @@ fn test_rounding_end() { new_ucmd!() .args(&["1", "-1", "0.1"]) .succeeds() - .stdout_is("1\n") - .no_stderr(); + .stdout_only("1\n"); } #[test] @@ -759,7 +710,7 @@ fn test_format_option() { } #[test] -#[ignore = "Need issue #6233 to be fixed"] +#[ignore = "Need issue #2660 to be fixed"] fn test_auto_precision() { new_ucmd!() .args(&["1", "0x1p-1", "2"]) @@ -768,7 +719,7 @@ fn test_auto_precision() { } #[test] -#[ignore = "Need issue #6234 to be fixed"] +#[ignore = "Need issue #3318 to be fixed"] fn test_undefined() { new_ucmd!() .args(&["1e-9223372036854775808"]) @@ -777,12 +728,22 @@ fn test_undefined() { } #[test] -#[ignore = "Need issue #6235 to be fixed"] fn test_invalid_float_point_fail_properly() { new_ucmd!() .args(&["66000e000000000000000000000000000000000000000000000000000009223372036854775807"]) .fails() - .stdout_only(""); // might need to be updated + .no_stdout() + .usage_error("invalid floating point argument: '66000e000000000000000000000000000000000000000000000000000009223372036854775807'"); + new_ucmd!() + .args(&["-1.1e9223372036854775807"]) + .fails() + .no_stdout() + .usage_error("invalid floating point argument: '-1.1e9223372036854775807'"); + new_ucmd!() + .args(&["-.1e9223372036854775807"]) + .fails() + .no_stdout() + .usage_error("invalid floating point argument: '-.1e9223372036854775807'"); } #[test] @@ -827,3 +788,31 @@ fn test_invalid_format() { .no_stdout() .stderr_contains("format '%g%g' has too many % directives"); } + +#[test] +fn test_parse_scientific_zero() { + new_ucmd!() + .args(&["0e15", "1"]) + .succeeds() + .stdout_only("0\n1\n"); + new_ucmd!() + .args(&["0.0e15", "1"]) + .succeeds() + .stdout_only("0\n1\n"); + new_ucmd!() + .args(&["0", "1"]) + .succeeds() + .stdout_only("0\n1\n"); + new_ucmd!() + .args(&["-w", "0e15", "1"]) + .succeeds() + .stdout_only("0000000000000000\n0000000000000001\n"); + new_ucmd!() + .args(&["-w", "0.0e15", "1"]) + .succeeds() + .stdout_only("0000000000000000\n0000000000000001\n"); + new_ucmd!() + .args(&["-w", "0", "1"]) + .succeeds() + .stdout_only("0\n1\n"); +} diff --git a/tests/by-util/test_sort.rs b/tests/by-util/test_sort.rs index 97bfc6a74..62aa07dae 100644 --- a/tests/by-util/test_sort.rs +++ b/tests/by-util/test_sort.rs @@ -3,7 +3,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (words) ints +// spell-checker:ignore (words) ints (linux) NOFILE #![allow(clippy::cast_possible_wrap)] use std::time::Duration; @@ -1084,6 +1084,31 @@ fn test_merge_batch_size() { .stdout_only_fixture("merge_ints_interleaved.expected"); } +#[test] +#[cfg(any(target_os = "linux", target_os = "android"))] +fn test_merge_batch_size_with_limit() { + use rlimit::Resource; + // Currently need... + // 3 descriptors for stdin, stdout, stderr + // 2 descriptors for CTRL+C handling logic (to be reworked at some point) + // 2 descriptors for the input files (i.e. batch-size of 2). + let limit_fd = 3 + 2 + 2; + TestScenario::new(util_name!()) + .ucmd() + .limit(Resource::NOFILE, limit_fd, limit_fd) + .arg("--batch-size=2") + .arg("-m") + .arg("--unique") + .arg("merge_ints_interleaved_1.txt") + .arg("merge_ints_interleaved_2.txt") + .arg("merge_ints_interleaved_3.txt") + .arg("merge_ints_interleaved_3.txt") + .arg("merge_ints_interleaved_2.txt") + .arg("merge_ints_interleaved_1.txt") + .succeeds() + .stdout_only_fixture("merge_ints_interleaved.expected"); +} + #[test] fn test_sigpipe_panic() { let mut cmd = new_ucmd!(); diff --git a/tests/by-util/test_stat.rs b/tests/by-util/test_stat.rs index 8cb4493f0..cbd36832f 100644 --- a/tests/by-util/test_stat.rs +++ b/tests/by-util/test_stat.rs @@ -242,7 +242,7 @@ fn test_multi_files() { #[test] fn test_printf() { let args = [ - "--printf=123%-# 15q\\r\\\"\\\\\\a\\b\\e\\f\\v%+020.23m\\x12\\167\\132\\112\\n", + "--printf=123%-# 15q\\r\\\"\\\\\\a\\b\\x1B\\f\\x0B%+020.23m\\x12\\167\\132\\112\\n", "/", ]; let ts = TestScenario::new(util_name!()); @@ -256,11 +256,10 @@ fn test_pipe_fifo() { let (at, mut ucmd) = at_and_ucmd!(); at.mkfifo("FIFO"); ucmd.arg("FIFO") - .run() + .succeeds() .no_stderr() .stdout_contains("fifo") - .stdout_contains("File: FIFO") - .succeeded(); + .stdout_contains("File: FIFO"); } #[test] @@ -275,19 +274,17 @@ fn test_stdin_pipe_fifo1() { new_ucmd!() .arg("-") .set_stdin(std::process::Stdio::piped()) - .run() + .succeeds() .no_stderr() .stdout_contains("fifo") - .stdout_contains("File: -") - .succeeded(); + .stdout_contains("File: -"); new_ucmd!() .args(&["-L", "-"]) .set_stdin(std::process::Stdio::piped()) - .run() + .succeeds() .no_stderr() .stdout_contains("fifo") - .stdout_contains("File: -") - .succeeded(); + .stdout_contains("File: -"); } #[test] @@ -299,11 +296,10 @@ fn test_stdin_pipe_fifo2() { new_ucmd!() .arg("-") .set_stdin(std::process::Stdio::null()) - .run() + .succeeds() .no_stderr() .stdout_contains("character special file") - .stdout_contains("File: -") - .succeeded(); + .stdout_contains("File: -"); } #[test] @@ -339,11 +335,10 @@ fn test_stdin_redirect() { ts.ucmd() .arg("-") .set_stdin(std::fs::File::open(at.plus("f")).unwrap()) - .run() + .succeeds() .no_stderr() .stdout_contains("regular empty file") - .stdout_contains("File: -") - .succeeded(); + .stdout_contains("File: -"); } #[test] @@ -352,3 +347,76 @@ fn test_without_argument() { .fails() .stderr_contains("missing operand\nTry 'stat --help' for more information."); } + +#[test] +fn test_quoting_style_locale() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + at.touch("'"); + ts.ucmd() + .env("QUOTING_STYLE", "locale") + .args(&["-c", "%N", "'"]) + .succeeds() + .stdout_only("'\\''\n"); + + ts.ucmd() + .args(&["-c", "%N", "'"]) + .succeeds() + .stdout_only("\"'\"\n"); +} + +#[test] +fn test_printf_octal_1() { + let ts = TestScenario::new(util_name!()); + let expected_stdout = vec![0x0A, 0xFF]; // Newline + byte 255 + ts.ucmd() + .args(&["--printf=\\012\\377", "."]) + .succeeds() + .stdout_is_bytes(expected_stdout); +} + +#[test] +fn test_printf_octal_2() { + let ts = TestScenario::new(util_name!()); + let expected_stdout = vec![b'.', 0x0A, b'a', 0xFF, b'b']; + ts.ucmd() + .args(&["--printf=.\\012a\\377b", "."]) + .succeeds() + .stdout_is_bytes(expected_stdout); +} + +#[test] +fn test_printf_incomplete_hex() { + let ts = TestScenario::new(util_name!()); + ts.ucmd() + .args(&["--printf=\\x", "."]) + .succeeds() + .stderr_contains("warning: incomplete hex escape"); +} + +#[test] +fn test_printf_bel_etc() { + let ts = TestScenario::new(util_name!()); + let expected_stdout = vec![0x07, 0x08, 0x0C, 0x0A, 0x0D, 0x09]; // BEL, BS, FF, LF, CR, TAB + ts.ucmd() + .args(&["--printf=\\a\\b\\f\\n\\r\\t", "."]) + .succeeds() + .stdout_is_bytes(expected_stdout); +} + +#[test] +fn test_printf_invalid_directive() { + let ts = TestScenario::new(util_name!()); + + ts.ucmd() + .args(&["--printf=%9", "."]) + .fails() + .code_is(1) + .stderr_contains("'%9': invalid directive"); + + ts.ucmd() + .args(&["--printf=%9%", "."]) + .fails() + .code_is(1) + .stderr_contains("'%9%': invalid directive"); +} diff --git a/tests/by-util/test_tail.rs b/tests/by-util/test_tail.rs index 4c7c52c7c..885e50ad3 100644 --- a/tests/by-util/test_tail.rs +++ b/tests/by-util/test_tail.rs @@ -6,6 +6,7 @@ // spell-checker:ignore (ToDO) abcdefghijklmnopqrstuvwxyz efghijklmnopqrstuvwxyz vwxyz emptyfile file siette ocho nueve diez MULT // spell-checker:ignore (libs) kqueue // spell-checker:ignore (jargon) tailable untailable datasame runneradmin tmpi +// spell-checker:ignore (cmd) taskkill #![allow( clippy::unicode_not_nfc, clippy::cast_lossless, @@ -4822,3 +4823,61 @@ fn test_obsolete_encoding_windows() { .stderr_is("tail: bad argument encoding: '-�b'\n") .code_is(1); } + +#[test] +#[cfg(not(target_vendor = "apple"))] // FIXME: for currently not working platforms +fn test_following_with_pid() { + use std::process::Command; + + let ts = TestScenario::new(util_name!()); + + #[cfg(not(windows))] + let mut sleep_command = Command::new("sleep") + .arg("999d") + .spawn() + .expect("failed to start sleep command"); + #[cfg(windows)] + let mut sleep_command = Command::new("powershell") + .arg("-Command") + .arg("Start-Sleep -Seconds 999") + .spawn() + .expect("failed to start sleep command"); + + let sleep_pid = sleep_command.id(); + + let at = &ts.fixtures; + at.touch("f"); + // when -f is specified, tail should die after + // the pid from --pid also dies + let mut child = ts + .ucmd() + .args(&[ + "--pid", + &sleep_pid.to_string(), + "-f", + at.plus("f").to_str().unwrap(), + ]) + .stderr_to_stdout() + .run_no_wait(); + child.make_assertion_with_delay(2000).is_alive(); + + #[cfg(not(windows))] + Command::new("kill") + .arg("-9") + .arg(sleep_pid.to_string()) + .output() + .expect("failed to kill sleep command"); + #[cfg(windows)] + Command::new("taskkill") + .arg("/PID") + .arg(sleep_pid.to_string()) + .arg("/F") + .output() + .expect("failed to kill sleep command"); + + let _ = sleep_command.wait(); + + child.make_assertion_with_delay(2000).is_not_alive(); + + child.kill(); +} diff --git a/tests/by-util/test_tee.rs b/tests/by-util/test_tee.rs index c32759ed4..4f2437ace 100644 --- a/tests/by-util/test_tee.rs +++ b/tests/by-util/test_tee.rs @@ -172,7 +172,7 @@ mod linux_only { let mut fds: [c_int; 2] = [0, 0]; assert!( - (unsafe { libc::pipe(&mut fds as *mut c_int) } == 0), + (unsafe { libc::pipe(std::ptr::from_mut::(&mut fds[0])) } == 0), "Failed to create pipe" ); diff --git a/tests/by-util/test_tr.rs b/tests/by-util/test_tr.rs index ebd7635e4..cd99f1c3a 100644 --- a/tests/by-util/test_tr.rs +++ b/tests/by-util/test_tr.rs @@ -13,6 +13,23 @@ fn test_invalid_arg() { new_ucmd!().arg("--definitely-invalid").fails().code_is(1); } +#[test] +fn test_invalid_input() { + new_ucmd!() + .args(&["1", "1", "<", "."]) + .fails() + .code_is(1) + .stderr_contains("tr: extra operand '<'"); + #[cfg(unix)] + new_ucmd!() + .args(&["1", "1"]) + // will test "tr 1 1 < ." + .set_stdin(std::process::Stdio::from(std::fs::File::open(".").unwrap())) + .fails() + .code_is(1) + .stderr_contains("tr: read error: Is a directory"); +} + #[test] fn test_to_upper() { new_ucmd!() @@ -1494,9 +1511,7 @@ fn test_multibyte_octal_sequence() { .args(&["-d", r"\501"]) .pipe_in("(1Ł)") .succeeds() - // TODO - // A warning needs to be printed here - // See https://github.com/uutils/coreutils/issues/6821 + .stderr_is("tr: warning: the ambiguous octal escape \\501 is being\n interpreted as the 2-byte sequence \\050, 1\n") .stdout_is("Ł)"); } diff --git a/tests/common/util.rs b/tests/common/util.rs index 87c937492..844618def 100644 --- a/tests/common/util.rs +++ b/tests/common/util.rs @@ -75,7 +75,7 @@ pub fn is_ci() -> bool { } /// Read a test scenario fixture, returning its bytes -fn read_scenario_fixture>(tmpd: &Option>, file_rel_path: S) -> Vec { +fn read_scenario_fixture>(tmpd: Option<&Rc>, file_rel_path: S) -> Vec { let tmpdir_path = tmpd.as_ref().unwrap().as_ref().path(); AtPath::new(tmpdir_path).read_bytes(file_rel_path.as_ref().to_str().unwrap()) } @@ -517,7 +517,7 @@ impl CmdResult { /// like `stdout_is()`, but expects the contents of the file at the provided relative path #[track_caller] pub fn stdout_is_fixture>(&self, file_rel_path: T) -> &Self { - let contents = read_scenario_fixture(&self.tmpd, file_rel_path); + let contents = read_scenario_fixture(self.tmpd.as_ref(), file_rel_path); self.stdout_is(String::from_utf8(contents).unwrap()) } @@ -539,7 +539,7 @@ impl CmdResult { /// ``` #[track_caller] pub fn stdout_is_fixture_bytes>(&self, file_rel_path: T) -> &Self { - let contents = read_scenario_fixture(&self.tmpd, file_rel_path); + let contents = read_scenario_fixture(self.tmpd.as_ref(), file_rel_path); self.stdout_is_bytes(contents) } @@ -552,7 +552,7 @@ impl CmdResult { template_vars: &[(&str, &str)], ) -> &Self { let mut contents = - String::from_utf8(read_scenario_fixture(&self.tmpd, file_rel_path)).unwrap(); + String::from_utf8(read_scenario_fixture(self.tmpd.as_ref(), file_rel_path)).unwrap(); for kv in template_vars { contents = contents.replace(kv.0, kv.1); } @@ -566,7 +566,8 @@ impl CmdResult { file_rel_path: T, template_vars: &[Vec<(String, String)>], ) { - let contents = String::from_utf8(read_scenario_fixture(&self.tmpd, file_rel_path)).unwrap(); + let contents = + String::from_utf8(read_scenario_fixture(self.tmpd.as_ref(), file_rel_path)).unwrap(); let possible_values = template_vars.iter().map(|vars| { let mut contents = contents.clone(); for kv in vars { @@ -604,7 +605,7 @@ impl CmdResult { /// Like `stdout_is_fixture`, but for stderr #[track_caller] pub fn stderr_is_fixture>(&self, file_rel_path: T) -> &Self { - let contents = read_scenario_fixture(&self.tmpd, file_rel_path); + let contents = read_scenario_fixture(self.tmpd.as_ref(), file_rel_path); self.stderr_is(String::from_utf8(contents).unwrap()) } @@ -629,7 +630,7 @@ impl CmdResult { /// like `stdout_only()`, but expects the contents of the file at the provided relative path #[track_caller] pub fn stdout_only_fixture>(&self, file_rel_path: T) -> &Self { - let contents = read_scenario_fixture(&self.tmpd, file_rel_path); + let contents = read_scenario_fixture(self.tmpd.as_ref(), file_rel_path); self.stdout_only_bytes(contents) } @@ -1384,7 +1385,7 @@ impl UCommand { /// like `pipe_in()`, but uses the contents of the file at the provided relative path as the piped in data pub fn pipe_in_fixture>(&mut self, file_rel_path: S) -> &mut Self { - let contents = read_scenario_fixture(&self.tmpd, file_rel_path); + let contents = read_scenario_fixture(self.tmpd.as_ref(), file_rel_path); self.pipe_in(contents) } diff --git a/tests/fixtures/dircolors/internal.expected b/tests/fixtures/dircolors/internal.expected index e151973f2..feea46455 100644 --- a/tests/fixtures/dircolors/internal.expected +++ b/tests/fixtures/dircolors/internal.expected @@ -7,6 +7,7 @@ # restrict following config to systems with matching environment variables. COLORTERM ?* TERM Eterm +TERM alacritty* TERM ansi TERM *color* TERM con[0-9]*x[0-9]* @@ -15,6 +16,7 @@ TERM console TERM cygwin TERM *direct* TERM dtterm +TERM foot TERM gnome TERM hurd TERM jfbterm diff --git a/tests/fixtures/sort/keys_closed_range.expected.debug b/tests/fixtures/sort/keys_closed_range.expected.debug index b78db4af1..e317d4079 100644 --- a/tests/fixtures/sort/keys_closed_range.expected.debug +++ b/tests/fixtures/sort/keys_closed_range.expected.debug @@ -11,8 +11,8 @@ ________ _ ________ 👩‍🔬 👩‍🔬 👩‍🔬 - __ -______________ + __ +________ 💣💣 💣💣 💣💣 __ ______________ diff --git a/tests/fixtures/sort/keys_multiple_ranges.expected.debug b/tests/fixtures/sort/keys_multiple_ranges.expected.debug index 830e9afd0..41b7e210d 100644 --- a/tests/fixtures/sort/keys_multiple_ranges.expected.debug +++ b/tests/fixtures/sort/keys_multiple_ranges.expected.debug @@ -15,9 +15,9 @@ ________ ___ ________ 👩‍🔬 👩‍🔬 👩‍🔬 - _____ - _____ -______________ + ___ + ___ +________ 💣💣 💣💣 💣💣 _____ _____ diff --git a/tests/fixtures/sort/keys_no_field_match.expected.debug b/tests/fixtures/sort/keys_no_field_match.expected.debug index 60197b1de..0a3ea8303 100644 --- a/tests/fixtures/sort/keys_no_field_match.expected.debug +++ b/tests/fixtures/sort/keys_no_field_match.expected.debug @@ -11,8 +11,8 @@ ________ ^ no match for key ________ 👩‍🔬 👩‍🔬 👩‍🔬 - ^ no match for key -______________ + ^ no match for key +________ 💣💣 💣💣 💣💣 ^ no match for key ______________ diff --git a/tests/fixtures/sort/keys_open_ended.expected.debug b/tests/fixtures/sort/keys_open_ended.expected.debug index d3a56ffd6..c8e4ad9ae 100644 --- a/tests/fixtures/sort/keys_open_ended.expected.debug +++ b/tests/fixtures/sort/keys_open_ended.expected.debug @@ -11,8 +11,8 @@ ________ ____ ________ 👩‍🔬 👩‍🔬 👩‍🔬 - _______ -______________ + _____ +________ 💣💣 💣💣 💣💣 _______ ______________ diff --git a/util/build-gnu.sh b/util/build-gnu.sh index 684187733..16868af4d 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -204,6 +204,9 @@ sed -i "s|cp: target directory 'symlink': Permission denied|cp: 'symlink' is not # Our message is a bit better sed -i "s|cannot create regular file 'no-such/': Not a directory|'no-such/' is not a directory|" tests/mv/trailing-slash.sh +# Our message is better +sed -i "s|warning: unrecognized escape|warning: incomplete hex escape|" tests/stat/stat-printf.pl + sed -i 's|cp |/usr/bin/cp |' tests/mv/hard-2.sh sed -i 's|paste |/usr/bin/paste |' tests/od/od-endian.sh sed -i 's|timeout |'"${SYSTEM_TIMEOUT}"' |' tests/tail/follow-stdin.sh @@ -264,7 +267,7 @@ sed -i "s/\(\(b2[ml]_[69]\|b32h_[56]\|z85_8\|z85_35\).*OUT=>\)[^}]*\(.*\)/\1\"\" sed -i "s/\$prog: invalid input/\$prog: error: invalid input/g" tests/basenc/basenc.pl # basenc: swap out error message for unexpected arg -sed -i "s/ {ERR=>\"\$prog: foobar\\\\n\" \. \$try_help }/ {ERR=>\"error: Found argument '--foobar' which wasn't expected, or isn't valid in this context\n\n If you tried to supply '--foobar' as a value rather than a flag, use '-- --foobar'\n\nUsage: basenc [OPTION]... [FILE]\n\nFor more information try '--help'\n\"}]/" tests/basenc/basenc.pl +sed -i "s/ {ERR=>\"\$prog: foobar\\\\n\" \. \$try_help }/ {ERR=>\"error: unexpected argument '--foobar' found\n\n tip: to pass '--foobar' as a value, use '-- --foobar'\n\nUsage: basenc [OPTION]... [FILE]\n\nFor more information, try '--help'.\n\"}]/" tests/basenc/basenc.pl sed -i "s/ {ERR_SUBST=>\"s\/(unrecognized|unknown) option \[-' \]\*foobar\[' \]\*\/foobar\/\"}],//" tests/basenc/basenc.pl # Remove the check whether a util was built. Otherwise tests against utils like "arch" are not run. @@ -311,7 +314,7 @@ sed -i -e "s|mv: cannot overwrite 'a/t': Directory not empty|mv: cannot move 'b/ # disable these test cases sed -i -E "s|^([^#]*2_31.*)$|#\1|g" tests/printf/printf-cov.pl -sed -i -e "s/du: invalid -t argument/du: invalid --threshold argument/" -e "s/du: option requires an argument/error: a value is required for '--threshold ' but none was supplied/" -e "/Try 'du --help' for more information./d" tests/du/threshold.sh +sed -i -e "s/du: invalid -t argument/du: invalid --threshold argument/" -e "s/du: option requires an argument/error: a value is required for '--threshold ' but none was supplied/" -e "s/Try 'du --help' for more information./\nFor more information, try '--help'./" tests/du/threshold.sh # Remove the extra output check sed -i -e "s|Try '\$prog --help' for more information.\\\n||" tests/du/files0-from.pl diff --git a/util/gnu-patches/tests_comm.pl.patch b/util/gnu-patches/tests_comm.pl.patch new file mode 100644 index 000000000..d3d5595a2 --- /dev/null +++ b/util/gnu-patches/tests_comm.pl.patch @@ -0,0 +1,44 @@ +diff --git a/tests/misc/comm.pl b/tests/misc/comm.pl +index 5bd5f56d7..8322d92ba 100755 +--- a/tests/misc/comm.pl ++++ b/tests/misc/comm.pl +@@ -73,18 +73,24 @@ my @Tests = + + # invalid missing command line argument (1) + ['missing-arg1', $inputs[0], {EXIT=>1}, +- {ERR => "$prog: missing operand after 'a'\n" +- . "Try '$prog --help' for more information.\n"}], ++ {ERR => "error: the following required arguments were not provided:\n" ++ . " \n\n" ++ . "Usage: $prog [OPTION]... FILE1 FILE2\n\n" ++ . "For more information, try '--help'.\n"}], + + # invalid missing command line argument (both) + ['missing-arg2', {EXIT=>1}, +- {ERR => "$prog: missing operand\n" +- . "Try '$prog --help' for more information.\n"}], ++ {ERR => "error: the following required arguments were not provided:\n" ++ . " \n" ++ . " \n\n" ++ . "Usage: $prog [OPTION]... FILE1 FILE2\n\n" ++ . "For more information, try '--help'.\n"}], + + # invalid extra command line argument + ['extra-arg', @inputs, 'no-such', {EXIT=>1}, +- {ERR => "$prog: extra operand 'no-such'\n" +- . "Try '$prog --help' for more information.\n"}], ++ {ERR => "error: unexpected argument 'no-such' found\n\n" ++ . "Usage: $prog [OPTION]... FILE1 FILE2\n\n" ++ . "For more information, try '--help'.\n"}], + + # out-of-order input + ['ooo', {IN=>{a=>"1\n3"}}, {IN=>{b=>"3\n2"}}, {EXIT=>1}, +@@ -163,7 +169,7 @@ my @Tests = + + # invalid dual delimiter + ['delim-dual', '--output-delimiter=,', '--output-delimiter=+', @inputs, +- {EXIT=>1}, {ERR => "$prog: multiple output delimiters specified\n"}], ++ {EXIT=>1}, {ERR => "$prog: multiple conflicting output delimiters specified\n"}], + + # valid dual delimiter specification + ['delim-dual2', '--output-delimiter=,', '--output-delimiter=,', @inputs, diff --git a/util/gnu-patches/tests_cut_error_msg.patch b/util/gnu-patches/tests_cut_error_msg.patch new file mode 100644 index 000000000..3f57d2048 --- /dev/null +++ b/util/gnu-patches/tests_cut_error_msg.patch @@ -0,0 +1,72 @@ +diff --git a/tests/cut/cut.pl b/tests/cut/cut.pl +index 1670db02e..ed633792a 100755 +--- a/tests/cut/cut.pl ++++ b/tests/cut/cut.pl +@@ -29,13 +29,15 @@ my $mb_locale = $ENV{LOCALE_FR_UTF8}; + + my $prog = 'cut'; + my $try = "Try '$prog --help' for more information.\n"; +-my $from_field1 = "$prog: fields are numbered from 1\n$try"; +-my $from_pos1 = "$prog: byte/character positions are numbered from 1\n$try"; +-my $inval_fld = "$prog: invalid field range\n$try"; +-my $inval_pos = "$prog: invalid byte or character range\n$try"; +-my $no_endpoint = "$prog: invalid range with no endpoint: -\n$try"; +-my $nofield = "$prog: an input delimiter may be specified only when " . +- "operating on fields\n$try"; ++my $from_field1 = "$prog: range '' was invalid: failed to parse range\n"; ++my $from_field_0 = "$prog: range '0' was invalid: fields and positions are numbered from 1\n"; ++my $from_field_0_dash = "$prog: range '0-' was invalid: fields and positions are numbered from 1\n"; ++my $from_field_0_2 = "$prog: range '0-2' was invalid: fields and positions are numbered from 1\n"; ++my $from_pos1 = "$prog: range '' was invalid: failed to parse range\n"; ++my $inval_fld = "$prog: range '--' was invalid: failed to parse range\n"; ++my $inval_pos = "$prog: range '--' was invalid: failed to parse range\n"; ++my $no_endpoint = "$prog: range '-' was invalid: invalid range with no endpoint\n"; ++my $nofield = "$prog: invalid input: The '--delimiter' ('-d') option only usable if printing a sequence of fields\n"; + + my @Tests = + ( +@@ -44,16 +46,16 @@ my @Tests = + + # This failed (as it should) even before coreutils-6.9.90, + # but cut from 6.9.90 produces a more useful diagnostic. +- ['zero-1', '-b0', {ERR=>$from_pos1}, {EXIT => 1} ], ++ ['zero-1', '-b0', {ERR=>$from_field_0}, {EXIT => 1} ], + + # Up to coreutils-6.9, specifying a range of 0-2 was not an error. + # It was treated just like "-2". +- ['zero-2', '-f0-2', {ERR=>$from_field1}, {EXIT => 1} ], ++ ['zero-2', '-f0-2', {ERR=>$from_field_0_2}, {EXIT => 1} ], + + # Up to coreutils-8.20, specifying a range of 0- was not an error. +- ['zero-3b', '-b0-', {ERR=>$from_pos1}, {EXIT => 1} ], +- ['zero-3c', '-c0-', {ERR=>$from_pos1}, {EXIT => 1} ], +- ['zero-3f', '-f0-', {ERR=>$from_field1}, {EXIT => 1} ], ++ ['zero-3b', '-b0-', {ERR=>$from_field_0_dash}, {EXIT => 1} ], ++ ['zero-3c', '-c0-', {ERR=>$from_field_0_dash}, {EXIT => 1} ], ++ ['zero-3f', '-f0-', {ERR=>$from_field_0_dash}, {EXIT => 1} ], + + ['1', '-d:', '-f1,3-', {IN=>"a:b:c\n"}, {OUT=>"a:c\n"}], + ['2', '-d:', '-f1,3-', {IN=>"a:b:c\n"}, {OUT=>"a:c\n"}], +@@ -96,11 +98,10 @@ my @Tests = + # Errors + # -s may be used only with -f + ['y', qw(-s -b4), {IN=>":\n"}, {OUT=>""}, {EXIT=>1}, +- {ERR=>"$prog: suppressing non-delimited lines makes sense\n" +- . "\tonly when operating on fields\n$try"}], ++ {ERR=>"$prog: invalid input: The '--only-delimited' ('-s') option only usable if printing a sequence of fields\n"}], + # You must specify bytes or fields (or chars) + ['z', '', {IN=>":\n"}, {OUT=>""}, {EXIT=>1}, +- {ERR=>"$prog: you must specify a list of bytes, characters, or fields\n$try"} ++ {ERR=>"$prog: invalid usage: expects one of --fields (-f), --chars (-c) or --bytes (-b)\n"} + ], + # Empty field list + ['empty-fl', qw(-f ''), {IN=>":\n"}, {OUT=>""}, {EXIT=>1}, +@@ -199,7 +200,7 @@ my @Tests = + + # None of the following invalid ranges provoked an error up to coreutils-6.9. + ['inval1', qw(-f 2-0), {IN=>''}, {OUT=>''}, {EXIT=>1}, +- {ERR=>"$prog: invalid decreasing range\n$try"}], ++ {ERR=>"$prog: range '2-0' was invalid: fields and positions are numbered from 1\n"}], + ['inval2', qw(-f -), {IN=>''}, {OUT=>''}, {EXIT=>1}, {ERR=>$no_endpoint}], + ['inval3', '-f', '4,-', {IN=>''}, {OUT=>''}, {EXIT=>1}, {ERR=>$no_endpoint}], + ['inval4', '-f', '1-2,-', {IN=>''}, {OUT=>''}, {EXIT=>1}, diff --git a/util/gnu-patches/tests_dup_source.patch b/util/gnu-patches/tests_dup_source.patch new file mode 100644 index 000000000..44e33723b --- /dev/null +++ b/util/gnu-patches/tests_dup_source.patch @@ -0,0 +1,13 @@ +diff --git a/tests/mv/dup-source.sh b/tests/mv/dup-source.sh +index 7bcd82fc3..0f9005296 100755 +--- a/tests/mv/dup-source.sh ++++ b/tests/mv/dup-source.sh +@@ -83,7 +83,7 @@ $i: cannot stat 'a': No such file or directory + $i: cannot stat 'a': No such file or directory + $i: cannot stat 'b': No such file or directory + $i: cannot move './b' to a subdirectory of itself, 'b/b' +-$i: warning: source directory 'b' specified more than once ++$i: cannot move 'b' to a subdirectory of itself, 'b/b' + EOF + compare exp out || fail=1 + done diff --git a/util/gnu-patches/tests_factor_factor.pl.patch b/util/gnu-patches/tests_factor_factor.pl.patch index fc8b988fe..731abcc91 100644 --- a/util/gnu-patches/tests_factor_factor.pl.patch +++ b/util/gnu-patches/tests_factor_factor.pl.patch @@ -1,8 +1,8 @@ diff --git a/tests/factor/factor.pl b/tests/factor/factor.pl -index 6e612e418..f19c06ca0 100755 +index b1406c266..3d97cd6a5 100755 --- a/tests/factor/factor.pl +++ b/tests/factor/factor.pl -@@ -61,12 +61,13 @@ my @Tests = +@@ -61,12 +61,14 @@ my @Tests = # Map newer glibc diagnostic to expected. # Also map OpenBSD 5.1's "unknown option" to expected "invalid option". {ERR_SUBST => q!s/'1'/1/;s/unknown/invalid/!}, @@ -10,7 +10,8 @@ index 6e612e418..f19c06ca0 100755 - . "Try '$prog --help' for more information.\n"}, + {ERR => "error: unexpected argument '-1' found\n\n" + . " tip: to pass '-1' as a value, use '-- -1'\n\n" -+ . "Usage: factor [OPTION]... [NUMBER]...\n"}, ++ . "Usage: factor [OPTION]... [NUMBER]...\n\n" ++ . "For more information, try '--help'.\n"}, {EXIT => 1}], ['cont', 'a 4', {OUT => "4: 2 2\n"}, diff --git a/util/gnu-patches/tests_ls_no_cap.patch b/util/gnu-patches/tests_ls_no_cap.patch new file mode 100644 index 000000000..5944e3f56 --- /dev/null +++ b/util/gnu-patches/tests_ls_no_cap.patch @@ -0,0 +1,22 @@ +diff --git a/tests/ls/no-cap.sh b/tests/ls/no-cap.sh +index 3d84c74ff..d1f60e70a 100755 +--- a/tests/ls/no-cap.sh ++++ b/tests/ls/no-cap.sh +@@ -21,13 +21,13 @@ print_ver_ ls + require_strace_ capget + + LS_COLORS=ca=1; export LS_COLORS +-strace -e capget ls --color=always > /dev/null 2> out || fail=1 +-$EGREP 'capget\(' out || skip_ "your ls doesn't call capget" ++strace -e llistxattr ls --color=always > /dev/null 2> out || fail=1 ++$EGREP 'llistxattr\(' out || skip_ "your ls doesn't call llistxattr" + + rm -f out + + LS_COLORS=ca=:; export LS_COLORS +-strace -e capget ls --color=always > /dev/null 2> out || fail=1 +-$EGREP 'capget\(' out && fail=1 ++strace -e llistxattr ls --color=always > /dev/null 2> out || fail=1 ++$EGREP 'llistxattr\(' out && fail=1 + + Exit $fail