diff --git a/.clippy.toml b/.clippy.toml index 89fd1cccd7f..6339ccf21b4 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 c8993b121eb..c4bcf51115d 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,19 @@ 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: Install/setup prerequisites + shell: bash + run: | + sudo apt-get -y update ; sudo apt-get -y install libselinux1-dev - name: Initialize workflow variables id: vars shell: bash @@ -139,7 +149,7 @@ jobs: shell: bash run: | RUSTDOCFLAGS="-Dwarnings" cargo doc ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} --no-deps --workspace --document-private-items - - uses: DavidAnson/markdownlint-cli2-action@v17 + - uses: DavidAnson/markdownlint-cli2-action@v19 with: fix: "true" globs: | @@ -159,6 +169,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 +178,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 @@ -191,6 +203,11 @@ jobs: # * ensure '.clippy.toml' MSRV configuration setting is equal to ${{ env.RUST_MIN_SRV }} CLIPPY_MSRV=$(grep -P "(?i)^\s*msrv\s*=\s*" .clippy.toml | grep -oP "\d+([.]\d+)+") if [ "${CLIPPY_MSRV}" != "${{ env.RUST_MIN_SRV }}" ]; then { echo "::error file=.clippy.toml::Incorrect MSRV configuration for clippy (found '${CLIPPY_MSRV}'; should be '${{ env.RUST_MIN_SRV }}'); update '.clippy.toml' with 'msrv = \"${{ env.RUST_MIN_SRV }}\"'" ; exit 1 ; } ; fi + - name: Install/setup prerequisites + shell: bash + run: | + # Install a package for one of the tests + sudo apt-get -y update ; sudo apt-get -y install attr - name: Info shell: bash run: | @@ -227,6 +244,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 +269,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 +325,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 +354,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 +380,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 +424,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 @@ -468,13 +495,13 @@ jobs: matrix: job: # - { os , target , cargo-options , features , use-cross , toolchain, skip-tests } - - { os: ubuntu-latest , target: arm-unknown-linux-gnueabihf, features: feat_os_unix_gnueabihf, use-cross: use-cross, skip-tests: true } + - { os: ubuntu-latest , target: arm-unknown-linux-gnueabihf , features: feat_os_unix_gnueabihf , use-cross: use-cross , skip-tests: true } - { os: ubuntu-latest , target: aarch64-unknown-linux-gnu , features: feat_os_unix_gnueabihf , use-cross: use-cross , skip-tests: true } - { os: ubuntu-latest , target: aarch64-unknown-linux-musl , features: feat_os_unix_musl , use-cross: use-cross , skip-tests: true } # - { os: ubuntu-latest , target: x86_64-unknown-linux-gnu , features: feat_selinux , use-cross: use-cross } - - { os: ubuntu-latest , target: i686-unknown-linux-gnu , features: feat_os_unix , use-cross: use-cross } + - { os: ubuntu-latest , target: i686-unknown-linux-gnu , features: "feat_os_unix,test_risky_names", use-cross: use-cross } - { os: ubuntu-latest , target: i686-unknown-linux-musl , features: feat_os_unix_musl , use-cross: use-cross } - - { os: ubuntu-latest , target: x86_64-unknown-linux-gnu , features: feat_os_unix , use-cross: use-cross } + - { os: ubuntu-latest , target: x86_64-unknown-linux-gnu , features: "feat_os_unix,test_risky_names", use-cross: use-cross } - { os: ubuntu-latest , target: x86_64-unknown-linux-musl , features: feat_os_unix_musl , use-cross: use-cross } - { os: ubuntu-latest , target: x86_64-unknown-redox , features: feat_os_unix_redox , use-cross: redoxer , skip-tests: true } - { os: macos-latest , target: aarch64-apple-darwin , features: feat_os_macos } # M1 CPU @@ -485,6 +512,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 +522,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 +782,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 +809,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 +891,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 +968,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 @@ -944,3 +980,27 @@ jobs: echo "Building and testing $f" cargo test -p "uu_$f" || exit 1 done + + test_all_features: + name: Test all features separately + needs: [ min_version, deps ] + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + # windows-latest - https://github.com/uutils/coreutils/issues/7044 + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - name: build and test all features individually + shell: bash + run: | + for f in $(util/show-utils.sh) + do + echo "Running tests with --features=$f and --no-default-features" + cargo test --features=$f --no-default-features + done diff --git a/.github/workflows/CheckScripts.yml b/.github/workflows/CheckScripts.yml index c18c4733cbe..4800cd2857d 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 e837b354687..5cd7fe647f2 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 36c54490ce9..987343723f6 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 113cb1e9745..0b9d8ce7fe4 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 d920ad80187..a7dcbdbbd45 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 cd1334c2e35..c4a166493c3 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; @@ -108,7 +112,7 @@ jobs: fault_type="${{ steps.vars.outputs.FAULT_TYPE }}" fault_prefix=$(echo "$fault_type" | tr '[:lower:]' '[:upper:]') # * convert any warnings to GHA UI annotations; ref: - S=$(cargo clippy --all-targets --features ${{ matrix.job.features }} -pcoreutils -- ${CLIPPY_FLAGS} -D warnings 2>&1) && printf "%s\n" "$S" || { printf "%s\n" "$S" ; printf "%s" "$S" | sed -E -n -e '/^error:/{' -e "N; s/^error:[[:space:]]+(.*)\\n[[:space:]]+-->[[:space:]]+(.*):([0-9]+):([0-9]+).*$/::${fault_type} file=\2,line=\3,col=\4::${fault_prefix}: \`cargo clippy\`: \1 (file:'\2', line:\3)/p;" -e '}' ; fault=true ; } + S=$(cargo clippy --all-targets --features ${{ matrix.job.features }} --tests -pcoreutils -- ${CLIPPY_FLAGS} -D warnings 2>&1) && printf "%s\n" "$S" || { printf "%s\n" "$S" ; printf "%s" "$S" | sed -E -n -e '/^error:/{' -e "N; s/^error:[[:space:]]+(.*)\\n[[:space:]]+-->[[:space:]]+(.*):([0-9]+):([0-9]+).*$/::${fault_type} file=\2,line=\3,col=\4::${fault_prefix}: \`cargo clippy\`: \1 (file:'\2', line:\3)/p;" -e '}' ; fault=true ; } if [ -n "${{ steps.vars.outputs.FAIL_ON_FAULT }}" ] && [ -n "$fault" ]; then exit 1 ; fi style_spellcheck: @@ -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 b31ac335328..4c43b77d7f3 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.8 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.8 with: usesh: true sync: rsync diff --git a/.github/workflows/fuzzing.yml b/.github/workflows/fuzzing.yml index df40b123679..c8e2c801408 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 @@ -55,6 +57,7 @@ jobs: - { name: fuzz_split, should_pass: false } - { name: fuzz_tr, should_pass: false } - { name: fuzz_env, should_pass: false } + - { name: fuzz_cksum, should_pass: false } - { name: fuzz_parse_glob, should_pass: true } - { name: fuzz_parse_size, should_pass: true } - { name: fuzz_parse_time, should_pass: true } @@ -62,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 c2e01f508e3..dc9e372d8c4 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 @@ -86,6 +88,7 @@ nolinks nonblock nonportable nonprinting +nonseekable notrunc noxfer ofile @@ -108,7 +111,9 @@ seedable semver semiprime semiprimes +setcap setfacl +setfattr shortcode shortcodes siginfo @@ -142,6 +147,8 @@ xattrs consts deps dev +fdlimit +inacc maint proc procs @@ -157,6 +164,8 @@ retval subdir val vals +inval +nofield # * clippy uninlined diff --git a/Cargo.lock b/Cargo.lock index b59405071e6..1b2a67c13aa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,22 +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" - -[[package]] -name = "ahash" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy", -] +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] name = "aho-corasick" @@ -31,9 +19,9 @@ dependencies = [ [[package]] name = "allocator-api2" -version = "0.2.18" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "android-tzdata" @@ -56,89 +44,90 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "219e3ce6f2611d83b51ec2098a12702112c29e57203a6b0a0929b2cddb486608" dependencies = [ - "unicode-width 0.1.13", + "unicode-width 0.1.14", ] [[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" -version = "0.2.0" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.0" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[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]] name = "arbitrary" -version = "1.3.2" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" dependencies = [ "derive_arbitrary", ] [[package]] name = "arrayref" -version = "0.3.6" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" [[package]] name = "arrayvec" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "autocfg" -version = "1.1.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[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", @@ -171,10 +160,10 @@ version = "0.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.7.0", "cexpr", "clang-sys", - "itertools", + "itertools 0.13.0", "log", "prettyplease", "proc-macro2", @@ -182,24 +171,9 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.86", -] - -[[package]] -name = "bit-set" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" -dependencies = [ - "bit-vec", + "syn", ] -[[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" @@ -208,9 +182,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "1be3f42a67d6d345ecd59f675f3f012d6974981560836e938c22b424b85ce1be" [[package]] name = "bitvec" @@ -237,9 +211,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", @@ -250,18 +224,18 @@ dependencies = [ [[package]] name = "block-buffer" -version = "0.10.3" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] [[package]] name = "bstr" -version = "1.10.0" +version = "1.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c" +checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0" dependencies = [ "memchr", "regex-automata", @@ -270,9 +244,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" @@ -288,9 +262,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cc" -version = "1.1.13" +version = "1.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72db2f7947ecee9b03b510377e8bb9077afa27176fdbff55c51027e976fdcc48" +checksum = "ad0cf6e91fde44c773c6ee7ec6bba798504641a8bc2eb7e37a04ffbf4dfaa55a" dependencies = [ "shlex", ] @@ -318,9 +292,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", @@ -328,11 +302,32 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "chrono-tz" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c6ac4f2c0bf0f44e9161aec9675e1050aa4a530663c4a9e37e108fa948bca9f" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf", +] + +[[package]] +name = "chrono-tz-build" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94fea34d77a245229e7746bd2beb786cd2a896f306ff491fb8cecb3074b10a7" +dependencies = [ + "parse-zoneinfo", + "phf_codegen", +] + [[package]] name = "clang-sys" -version = "1.4.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa2e27ae6ab525c3d369ded447057bca5438d86dc3a68f6faafb8269ba82ebf3" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" dependencies = [ "glob", "libc", @@ -341,46 +336,46 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.2" +version = "4.5.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a13b88d2c62ff462f88e4a121f17a82c1af05693a2f192b5c38d14de73c19f6" +checksum = "a8eb5e908ef3a6efbe1ed62520fb7287959888c88485abe072543190ecc66783" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.4.2" +version = "4.5.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bb9faaa7c2ef94b2743a21f5a29e6f0010dff4caa69ac8e9d6cf8b6fa74da08" +checksum = "96b01801b5fc6a0a232407abc821660c9c6d25a1cafc0d4f85f29fb8d9afc121" 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.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "586a385f7ef2f8b4d86bddaa0c094794e7ccbfe5ffef1f434fe928143fc783a5" +checksum = "33a7e468e750fa4b6be660e8b5651ad47372e8fb114030b594c2d75d48c5ffd0" 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.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0f09a0ca8f0dd8ac92c546b426f466ef19828185c6d504c80c48c9c2768ed9" +checksum = "724842fa9b144f9b89b3f3d371a89f3455eea660361d13a554f68f8ae5d6c13a" dependencies = [ "clap", "roff", @@ -388,9 +383,9 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "compare" @@ -400,22 +395,22 @@ checksum = "120133d4db2ec47efe2e26502ee984747630c67f51974fca0b6c1340cf2368d3" [[package]] name = "console" -version = "0.15.8" +version = "0.15.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +checksum = "ea3c6ecd8059b57859df5c69830340ed3c41d30e3da0c1cbed90a96ac853041b" dependencies = [ "encode_unicode", - "lazy_static", "libc", - "unicode-width 0.1.13", - "windows-sys 0.52.0", + "once_cell", + "unicode-width 0.2.0", + "windows-sys 0.59.0", ] [[package]] name = "const-random" -version = "0.1.16" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11df32a13d7892ec42d51d3d175faba5211ffe13ed25d4fb348ac9e9ce835593" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" dependencies = [ "const-random-macro", ] @@ -433,19 +428,19 @@ 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" -version = "0.8.3" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "coreutils" -version = "0.0.28" +version = "0.0.29" dependencies = [ "bincode", "chrono", @@ -597,44 +592,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", "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", ] [[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,77 +637,64 @@ dependencies = [ "lazy_static", "proc-macro2", "quote", - "syn 2.0.86", + "syn", ] [[package]] name = "cpufeatures" -version = "0.2.5" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" dependencies = [ "libc", ] [[package]] name = "crc32fast" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crossbeam-channel" -version = "0.5.10" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82a9b73a36529d9c47029b9fb3a6f0ea3cc916a261195352ba19e770fc1748b2" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", - "crossbeam-utils", ] [[package]] name = "crossbeam-deque" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fca89a0e215bab21874660c67903c5f143333cab1da83d041c7ded6053774751" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ - "cfg-if", "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" -version = "0.9.17" +version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e3681d554572a651dda4186cd47240627c3d0114d45a95f6ad27f2f22e7548d" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ - "autocfg", - "cfg-if", "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.19" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[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", + "bitflags 2.7.0", "crossterm_winapi", "filedescriptor", - "libc", "mio", "parking_lot", + "rustix 0.38.43", "signal-hook", "signal-hook-mio", "winapi", @@ -755,15 +737,15 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" +checksum = "0e60eed09d8c01d3cee5b7d30acb059b76614c918fa0f992e0dd6eeb10daad6f" [[package]] name = "data-encoding-macro" -version = "0.1.15" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1559b6cba622276d6d63706db152618eeb15b89b3e4041446b05876e352e639" +checksum = "5b16d9d0d88a5273d830dac8b78ceb217ffc9b1d5404e5597a3542515329405b" dependencies = [ "data-encoding", "data-encoding-macro-internal", @@ -771,12 +753,12 @@ dependencies = [ [[package]] name = "data-encoding-macro-internal" -version = "0.1.13" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "332d754c0af53bc87c108fed664d121ecf59207ec4196041f04d6ab9002ad33f" +checksum = "1145d32e826a7748b69ee8fc62d3e6355ff7f1051df53141e7048162fc90481b" dependencies = [ "data-encoding", - "syn 1.0.109", + "syn", ] [[package]] @@ -790,13 +772,13 @@ dependencies = [ [[package]] name = "derive_arbitrary" -version = "1.3.2" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" +checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn", ] [[package]] @@ -817,20 +799,20 @@ 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", ] [[package]] name = "dlv-list" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d529fd73d344663edfd598ccb3f344e46034db51ebd103518eae34338248ad73" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" dependencies = [ "const-random", ] @@ -855,15 +837,15 @@ checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] name = "either" -version = "1.8.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "encode_unicode" -version = "0.3.6" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" [[package]] name = "equivalent" @@ -873,12 +855,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.8" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -887,7 +869,7 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22be12de19decddab85d09f251ec8363f060ccb22ec9c81bc157c0c8433946d8" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.7.0", "log", "scopeguard", "uuid", @@ -895,9 +877,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.1.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "file_diff" @@ -912,7 +894,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7199d965852c3bac31f779ef99cbb4537f80e952e2d6aa0ffeb30cce00f4f46e" dependencies = [ "libc", - "thiserror", + "thiserror 1.0.69", "winapi", ] @@ -930,9 +912,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", @@ -944,6 +926,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" + [[package]] name = "fs_extra" version = "1.3.0" @@ -961,9 +949,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", @@ -990,54 +978,12 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" -[[package]] -name = "futures" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = [ - "futures-core", - "futures-sink", -] - [[package]] name = "futures-core" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" -[[package]] -name = "futures-executor" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - [[package]] name = "futures-macro" version = "0.3.31" @@ -1046,15 +992,9 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn", ] -[[package]] -name = "futures-sink" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - [[package]] name = "futures-task" version = "0.3.31" @@ -1073,13 +1013,9 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ - "futures-channel", "futures-core", - "futures-io", "futures-macro", - "futures-sink", "futures-task", - "memchr", "pin-project-lite", "pin-utils", "slab", @@ -1093,9 +1029,9 @@ checksum = "1d758ba1b47b00caf47f24925c0074ecb20d6dfcffe7f6d53395c0465674841a" [[package]] name = "generic-array" -version = "0.14.6" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", @@ -1103,9 +1039,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.9" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", @@ -1114,9 +1050,9 @@ dependencies = [ [[package]] name = "glob" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" [[package]] name = "half" @@ -1130,19 +1066,26 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.3" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" dependencies = [ - "ahash", "allocator-api2", + "equivalent", + "foldhash", ] [[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" @@ -1169,16 +1112,16 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.53" +version = "0.1.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "winapi", + "windows-core", ] [[package]] @@ -1192,12 +1135,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.5.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.15.2", ] [[package]] @@ -1215,11 +1158,11 @@ dependencies = [ [[package]] name = "inotify" -version = "0.9.6" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.7.0", "inotify-sys", "libc", ] @@ -1244,6 +1187,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" @@ -1253,35 +1202,45 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" -version = "1.0.4" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "js-sys" -version = "0.3.64" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" dependencies = [ + "once_cell", "wasm-bindgen", ] [[package]] name = "keccak" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f6d5ed8676d904364de097082f4e7d240b571b67989ced0240f08b7f966f940" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" dependencies = [ "cpufeatures", ] [[package]] name = "kqueue" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c8fc60ba15bf51257aa9807a48a61013db043fcf3a78cb0d916e8e396dcad98" +checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c" dependencies = [ "kqueue-sys", "libc", @@ -1289,9 +1248,9 @@ dependencies = [ [[package]] name = "kqueue-sys" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8367585489f01bc55dd27404dcf56b95e6da061a256a666ab23be9ba96a2e587" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" dependencies = [ "bitflags 1.3.2", "libc", @@ -1299,31 +1258,31 @@ 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" -version = "0.7.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", - "winapi", + "windows-targets 0.48.5", ] [[package]] name = "libm" -version = "0.2.7" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" [[package]] name = "libredox" @@ -1331,7 +1290,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.7.0", "libc", "redox_syscall", ] @@ -1344,33 +1303,39 @@ checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "linux-raw-sys" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "lock_api" -version = "0.4.9" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", ] +[[package]] +name = "lockfree-object-pool" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e" + [[package]] name = "log" -version = "0.4.20" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "lru" -version = "0.12.3" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" dependencies = [ - "hashbrown", + "hashbrown 0.15.2", ] [[package]] @@ -1416,23 +1381,23 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.2" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" dependencies = [ - "adler", + "adler2", ] [[package]] name = "mio" -version = "0.8.11" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", "log", "wasi", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -1441,7 +1406,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.7.0", "cfg-if", "cfg_aliases", "libc", @@ -1459,29 +1424,36 @@ dependencies = [ [[package]] name = "notify" -version = "6.0.1" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5738a2795d57ea20abec2d6d76c6081186709c0024187cd5977265eda6598b51" +checksum = "2fee8403b3d66ac7b26aee6e40a897d85dc5ce26f44da36b8b73e987cc52e943" dependencies = [ - "bitflags 1.3.2", - "crossbeam-channel", + "bitflags 2.7.0", "filetime", "fsevent-sys", "inotify", "kqueue", "libc", + "log", "mio", + "notify-types", "walkdir", - "windows-sys 0.45.0", + "windows-sys 0.59.0", ] +[[package]] +name = "notify-types" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d" + [[package]] name = "nu-ansi-term" -version = "0.50.0" +version = "0.50.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd2800e1520bdc966782168a627aa5d1ad92e33b984bf7c7615d31280c83ff14" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -1544,35 +1516,13 @@ 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]] name = "num_threads" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" dependencies = [ "libc", ] @@ -1618,7 +1568,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" dependencies = [ "dlv-list", - "hashbrown", + "hashbrown 0.14.5", ] [[package]] @@ -1627,14 +1577,14 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a6229bad892b46b0dcfaaeb18ad0d2e56400f5aaea05b768bde96e73676cf75" dependencies = [ - "unicode-width 0.1.13", + "unicode-width 0.1.14", ] [[package]] name = "parking_lot" -version = "0.12.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", @@ -1653,6 +1603,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "parse-zoneinfo" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24" +dependencies = [ + "regex", +] + [[package]] name = "parse_datetime" version = "0.6.0" @@ -1666,18 +1625,18 @@ dependencies = [ [[package]] name = "phf" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" dependencies = [ "phf_shared", ] [[package]] name = "phf_codegen" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" dependencies = [ "phf_generator", "phf_shared", @@ -1685,9 +1644,9 @@ dependencies = [ [[package]] name = "phf_generator" -version = "0.11.1" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1181c94580fa345f50f19d738aaa39c0ed30a600d95cb2d3e23f94266f14fbf" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared", "rand", @@ -1695,18 +1654,18 @@ dependencies = [ [[package]] name = "phf_shared" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" dependencies = [ "siphasher", ] [[package]] name = "pin-project-lite" -version = "0.2.9" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" @@ -1716,15 +1675,15 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.26" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[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", @@ -1732,9 +1691,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.6.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" +checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" [[package]] name = "powerfmt" @@ -1744,9 +1703,12 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.17" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] [[package]] name = "pretty_assertions" @@ -1760,12 +1722,12 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.19" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ac2cf0f2e4f42b49f5ffd07dae8d746508ef7526c13940e5f524012ae6c6550" +checksum = "483f8c21f64f3ea09fe0f30f5d48c3e8eefe5dac9129f0075f76593b4c1da705" dependencies = [ "proc-macro2", - "syn 2.0.86", + "syn", ] [[package]] @@ -1779,9 +1741,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.89" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" dependencies = [ "unicode-ident", ] @@ -1792,10 +1754,10 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc5b72d8145275d844d4b5f6d4e1eef00c8cd889edb6035c21675d1bb1f45c9f" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.7.0", "hex", "procfs-core", - "rustix 0.38.37", + "rustix 0.38.43", ] [[package]] @@ -1804,36 +1766,10 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "239df02d8349b06fc07398a3a1697b06418223b1c7725085e801e7c0fc6a12ec" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.7.0", "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 +1778,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 +1830,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" @@ -1925,18 +1852,18 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.7.0", ] [[package]] name = "reference-counted-singleton" -version = "0.1.2" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bfbf25d7eb88ddcbb1ec3d755d0634da8f7657b2cb8b74089121409ab8228f" +checksum = "5daffa8f5ca827e146485577fa9dba9bd9c6921e06e954ab8f6408c10f753086" [[package]] name = "regex" @@ -1952,9 +1879,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", @@ -1984,27 +1911,27 @@ dependencies = [ [[package]] name = "roff" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b833d8d034ea094b1ea68aa6d5c740e0d04bad9d16568d08ba6f76823a114316" +checksum = "88f8660c1ff60292143c98d08fc6e2f654d722db50410e3f3797d40baaf9d8f3" [[package]] name = "rstest" -version = "0.23.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a2c585be59b6b5dd66a9d2084aa1d8bd52fbdb806eafdeffb52791147862035" +checksum = "03e905296805ab93e13c1ec3a03f4b6c4f35e9498a3d5fa96dc626d22c03cd89" dependencies = [ - "futures", "futures-timer", + "futures-util", "rstest_macros", "rustc_version", ] [[package]] name = "rstest_macros" -version = "0.23.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "825ea780781b15345a146be27eaefb05085e337e869bff01b4306a4fd4a9ad5a" +checksum = "ef0053bbffce09062bee4bcc499b0fbe7a57b879f1efe088d6d8d4c7adcdef9b" dependencies = [ "cfg-if", "glob", @@ -2014,7 +1941,7 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn 2.0.86", + "syn", "unicode-ident", ] @@ -2046,9 +1973,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.37.26" +version = "0.37.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84f3f8f960ed3b5a59055428714943298bf3fa2d4a1d53135084e0544829d995" +checksum = "519165d378b97752ca44bbe15047d5d3409e875f39327546b42ac81d7e18c1b6" dependencies = [ "bitflags 1.3.2", "errno", @@ -2060,27 +1987,15 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.37" +version = "0.38.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" +checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.7.0", "errno", "libc", - "linux-raw-sys 0.4.14", - "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", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", ] [[package]] @@ -2100,9 +2015,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" @@ -2110,19 +2025,19 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0139b2436c81305eb6bda33af151851f75bd62783817b25f44daa371119c30b5" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.7.0", "libc", "once_cell", "reference-counted-singleton", "selinux-sys", - "thiserror", + "thiserror 1.0.69", ] [[package]] name = "selinux-sys" -version = "0.6.12" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d557667087c5b4791e180b80979cd1a92fdb9bfd92cfd4b9ab199c4d7402423" +checksum = "e5e6e2b8e07a8ff45c90f8e3611bf10c4da7a28d73a26f9ede04f927da234f52" dependencies = [ "bindgen", "cc", @@ -2132,15 +2047,15 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.14" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4" +checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" [[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 +2071,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", ] [[package]] @@ -2215,9 +2130,9 @@ 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", @@ -2226,24 +2141,30 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" 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" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "slab" -version = "0.4.7" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] @@ -2265,42 +2186,31 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "smawk" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043" +checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" [[package]] name = "socket2" -version = "0.5.3" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" dependencies = [ "libc", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - -[[package]] -name = "syn" -version = "1.0.109" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.86" +version = "2.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89275301d38033efb81a6e60e3497e734dfcc62571f2854bf4b16690398824c" +checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" dependencies = [ "proc-macro2", "quote", @@ -2315,14 +2225,15 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.13.0" +version = "3.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" +checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" dependencies = [ "cfg-if", "fastrand", + "getrandom", "once_cell", - "rustix 0.38.37", + "rustix 0.38.43", "windows-sys 0.59.0", ] @@ -2332,17 +2243,17 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e6bf6f19e9f8ed8d4048dc22981458ebcf406d67e94cd422e5ecd73d63b3237" dependencies = [ - "rustix 0.37.26", + "rustix 0.37.28", "windows-sys 0.48.0", ] [[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.43", "windows-sys 0.59.0", ] @@ -2355,34 +2266,54 @@ dependencies = [ "smawk", "terminal_size 0.2.6", "unicode-linebreak", - "unicode-width 0.1.13", + "unicode-width 0.1.14", ] [[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.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +dependencies = [ + "thiserror-impl 2.0.11", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] name = "thiserror-impl" -version = "1.0.66" +version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b08be0f17bd307950653ce45db00cd31200d82b624b36e181337d9c7d92765b5" +checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn", ] [[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 +2334,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", @@ -2445,21 +2376,15 @@ checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc" [[package]] name = "typenum" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" - -[[package]] -name = "unarray" -version = "0.1.4" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unicode-ident" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] name = "unicode-linebreak" @@ -2475,9 +2400,9 @@ checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-width" -version = "0.1.13" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "unicode-width" @@ -2487,9 +2412,9 @@ checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" [[package]] name = "unicode-xid" -version = "0.2.4" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "unindent" @@ -2499,9 +2424,9 @@ checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce" [[package]] name = "utf8parse" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "utmp-classic" @@ -2511,7 +2436,7 @@ checksum = "e24c654e19afaa6b8f3877ece5d3bed849c2719c56f6752b18ca7da4fcc6e85a" dependencies = [ "cfg-if", "libc", - "thiserror", + "thiserror 1.0.69", "time", "utmp-classic-raw", "zerocopy", @@ -2529,7 +2454,7 @@ dependencies = [ [[package]] name = "uu_arch" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "platform-info", @@ -2538,16 +2463,15 @@ dependencies = [ [[package]] name = "uu_base32" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", - "proptest", "uucore", ] [[package]] name = "uu_base64" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uu_base32", @@ -2556,7 +2480,7 @@ dependencies = [ [[package]] name = "uu_basename" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -2564,7 +2488,7 @@ dependencies = [ [[package]] name = "uu_basenc" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uu_base32", @@ -2573,29 +2497,29 @@ dependencies = [ [[package]] name = "uu_cat" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "nix", - "thiserror", + "thiserror 2.0.11", "uucore", ] [[package]] name = "uu_chcon" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "fts-sys", "libc", "selinux", - "thiserror", + "thiserror 2.0.11", "uucore", ] [[package]] name = "uu_chgrp" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -2603,7 +2527,7 @@ dependencies = [ [[package]] name = "uu_chmod" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "libc", @@ -2612,7 +2536,7 @@ dependencies = [ [[package]] name = "uu_chown" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -2620,7 +2544,7 @@ dependencies = [ [[package]] name = "uu_chroot" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -2628,7 +2552,7 @@ dependencies = [ [[package]] name = "uu_cksum" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "hex", @@ -2638,7 +2562,7 @@ dependencies = [ [[package]] name = "uu_comm" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -2646,14 +2570,14 @@ dependencies = [ [[package]] name = "uu_cp" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "exacl", "filetime", "indicatif", "libc", - "quick-error 2.0.1", + "quick-error", "selinux", "uucore", "walkdir", @@ -2662,17 +2586,17 @@ dependencies = [ [[package]] name = "uu_csplit" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "regex", - "thiserror", + "thiserror 2.0.11", "uucore", ] [[package]] name = "uu_cut" -version = "0.0.28" +version = "0.0.29" dependencies = [ "bstr", "clap", @@ -2682,10 +2606,12 @@ dependencies = [ [[package]] name = "uu_date" -version = "0.0.28" +version = "0.0.29" dependencies = [ "chrono", + "chrono-tz", "clap", + "iana-time-zone", "libc", "parse_datetime", "uucore", @@ -2694,7 +2620,7 @@ dependencies = [ [[package]] name = "uu_dd" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "gcd", @@ -2706,17 +2632,17 @@ dependencies = [ [[package]] name = "uu_df" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "tempfile", - "unicode-width 0.1.13", + "unicode-width 0.2.0", "uucore", ] [[package]] name = "uu_dir" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uu_ls", @@ -2725,7 +2651,7 @@ dependencies = [ [[package]] name = "uu_dircolors" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -2733,7 +2659,7 @@ dependencies = [ [[package]] name = "uu_dirname" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -2741,7 +2667,7 @@ dependencies = [ [[package]] name = "uu_du" -version = "0.0.28" +version = "0.0.29" dependencies = [ "chrono", "clap", @@ -2752,7 +2678,7 @@ dependencies = [ [[package]] name = "uu_echo" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -2760,7 +2686,7 @@ dependencies = [ [[package]] name = "uu_env" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "nix", @@ -2770,16 +2696,16 @@ dependencies = [ [[package]] name = "uu_expand" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", - "unicode-width 0.1.13", + "unicode-width 0.2.0", "uucore", ] [[package]] name = "uu_expr" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "num-bigint", @@ -2790,7 +2716,7 @@ dependencies = [ [[package]] name = "uu_factor" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "coz", @@ -2804,7 +2730,7 @@ dependencies = [ [[package]] name = "uu_false" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -2812,16 +2738,16 @@ dependencies = [ [[package]] name = "uu_fmt" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", - "unicode-width 0.1.13", + "unicode-width 0.2.0", "uucore", ] [[package]] name = "uu_fold" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -2829,7 +2755,7 @@ dependencies = [ [[package]] name = "uu_groups" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -2837,7 +2763,7 @@ dependencies = [ [[package]] name = "uu_hashsum" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "hex", @@ -2848,16 +2774,17 @@ dependencies = [ [[package]] name = "uu_head" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "memchr", + "thiserror 2.0.11", "uucore", ] [[package]] name = "uu_hostid" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "libc", @@ -2866,7 +2793,7 @@ dependencies = [ [[package]] name = "uu_hostname" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "dns-lookup", @@ -2877,7 +2804,7 @@ dependencies = [ [[package]] name = "uu_id" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "selinux", @@ -2886,7 +2813,7 @@ dependencies = [ [[package]] name = "uu_install" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "file_diff", @@ -2897,7 +2824,7 @@ dependencies = [ [[package]] name = "uu_join" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "memchr", @@ -2906,7 +2833,7 @@ dependencies = [ [[package]] name = "uu_kill" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "nix", @@ -2915,7 +2842,7 @@ dependencies = [ [[package]] name = "uu_link" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -2923,7 +2850,7 @@ dependencies = [ [[package]] name = "uu_ln" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -2931,7 +2858,7 @@ dependencies = [ [[package]] name = "uu_logname" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "libc", @@ -2940,7 +2867,7 @@ dependencies = [ [[package]] name = "uu_ls" -version = "0.0.28" +version = "0.0.29" dependencies = [ "ansi-width", "chrono", @@ -2951,14 +2878,14 @@ dependencies = [ "number_prefix", "once_cell", "selinux", - "terminal_size 0.4.0", + "terminal_size 0.4.1", "uucore", "uutils_term_grid", ] [[package]] name = "uu_mkdir" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -2966,7 +2893,7 @@ dependencies = [ [[package]] name = "uu_mkfifo" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "libc", @@ -2975,7 +2902,7 @@ dependencies = [ [[package]] name = "uu_mknod" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "libc", @@ -2984,7 +2911,7 @@ dependencies = [ [[package]] name = "uu_mktemp" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "rand", @@ -2994,19 +2921,19 @@ dependencies = [ [[package]] name = "uu_more" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "crossterm", "nix", "unicode-segmentation", - "unicode-width 0.1.13", + "unicode-width 0.2.0", "uucore", ] [[package]] name = "uu_mv" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "fs_extra", @@ -3016,7 +2943,7 @@ dependencies = [ [[package]] name = "uu_nice" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "libc", @@ -3026,7 +2953,7 @@ dependencies = [ [[package]] name = "uu_nl" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "regex", @@ -3035,7 +2962,7 @@ dependencies = [ [[package]] name = "uu_nohup" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "libc", @@ -3044,7 +2971,7 @@ dependencies = [ [[package]] name = "uu_nproc" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "libc", @@ -3053,7 +2980,7 @@ dependencies = [ [[package]] name = "uu_numfmt" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -3061,7 +2988,7 @@ dependencies = [ [[package]] name = "uu_od" -version = "0.0.28" +version = "0.0.29" dependencies = [ "byteorder", "clap", @@ -3071,7 +2998,7 @@ dependencies = [ [[package]] name = "uu_paste" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -3079,7 +3006,7 @@ dependencies = [ [[package]] name = "uu_pathchk" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "libc", @@ -3088,7 +3015,7 @@ dependencies = [ [[package]] name = "uu_pinky" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -3096,19 +3023,19 @@ dependencies = [ [[package]] name = "uu_pr" -version = "0.0.28" +version = "0.0.29" dependencies = [ "chrono", "clap", - "itertools", - "quick-error 2.0.1", + "itertools 0.14.0", + "quick-error", "regex", "uucore", ] [[package]] name = "uu_printenv" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -3116,7 +3043,7 @@ dependencies = [ [[package]] name = "uu_printf" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -3124,7 +3051,7 @@ dependencies = [ [[package]] name = "uu_ptx" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "regex", @@ -3133,7 +3060,7 @@ dependencies = [ [[package]] name = "uu_pwd" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -3141,7 +3068,7 @@ dependencies = [ [[package]] name = "uu_readlink" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -3149,7 +3076,7 @@ dependencies = [ [[package]] name = "uu_realpath" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -3157,7 +3084,7 @@ dependencies = [ [[package]] name = "uu_rm" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "libc", @@ -3168,7 +3095,7 @@ dependencies = [ [[package]] name = "uu_rmdir" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "libc", @@ -3177,18 +3104,18 @@ dependencies = [ [[package]] name = "uu_runcon" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "libc", "selinux", - "thiserror", + "thiserror 2.0.11", "uucore", ] [[package]] name = "uu_seq" -version = "0.0.28" +version = "0.0.29" dependencies = [ "bigdecimal", "clap", @@ -3199,7 +3126,7 @@ dependencies = [ [[package]] name = "uu_shred" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "libc", @@ -3209,7 +3136,7 @@ dependencies = [ [[package]] name = "uu_shuf" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "memchr", @@ -3220,7 +3147,7 @@ dependencies = [ [[package]] name = "uu_sleep" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "fundu", @@ -3229,27 +3156,27 @@ dependencies = [ [[package]] name = "uu_sort" -version = "0.0.28" +version = "0.0.29" dependencies = [ "binary-heap-plus", "clap", "compare", "ctrlc", "fnv", - "itertools", + "itertools 0.14.0", "memchr", "nix", "rand", "rayon", "self_cell", "tempfile", - "unicode-width 0.1.13", + "unicode-width 0.2.0", "uucore", ] [[package]] name = "uu_split" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "memchr", @@ -3258,7 +3185,7 @@ dependencies = [ [[package]] name = "uu_stat" -version = "0.0.28" +version = "0.0.29" dependencies = [ "chrono", "clap", @@ -3267,7 +3194,7 @@ dependencies = [ [[package]] name = "uu_stdbuf" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "tempfile", @@ -3277,7 +3204,7 @@ dependencies = [ [[package]] name = "uu_stdbuf_libstdbuf" -version = "0.0.28" +version = "0.0.29" dependencies = [ "cpp", "cpp_build", @@ -3286,7 +3213,7 @@ dependencies = [ [[package]] name = "uu_stty" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "nix", @@ -3295,7 +3222,7 @@ dependencies = [ [[package]] name = "uu_sum" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -3303,7 +3230,7 @@ dependencies = [ [[package]] name = "uu_sync" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "libc", @@ -3314,7 +3241,7 @@ dependencies = [ [[package]] name = "uu_tac" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "memchr", @@ -3325,7 +3252,7 @@ dependencies = [ [[package]] name = "uu_tail" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "fundu", @@ -3341,7 +3268,7 @@ dependencies = [ [[package]] name = "uu_tee" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "libc", @@ -3350,7 +3277,7 @@ dependencies = [ [[package]] name = "uu_test" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "libc", @@ -3359,7 +3286,7 @@ dependencies = [ [[package]] name = "uu_timeout" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "libc", @@ -3369,7 +3296,7 @@ dependencies = [ [[package]] name = "uu_touch" -version = "0.0.28" +version = "0.0.29" dependencies = [ "chrono", "clap", @@ -3381,7 +3308,7 @@ dependencies = [ [[package]] name = "uu_tr" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "nom", @@ -3390,7 +3317,7 @@ dependencies = [ [[package]] name = "uu_true" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -3398,7 +3325,7 @@ dependencies = [ [[package]] name = "uu_truncate" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -3406,7 +3333,7 @@ dependencies = [ [[package]] name = "uu_tsort" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -3414,7 +3341,7 @@ dependencies = [ [[package]] name = "uu_tty" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "nix", @@ -3423,7 +3350,7 @@ dependencies = [ [[package]] name = "uu_uname" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "platform-info", @@ -3432,16 +3359,16 @@ dependencies = [ [[package]] name = "uu_unexpand" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", - "unicode-width 0.1.13", + "unicode-width 0.2.0", "uucore", ] [[package]] name = "uu_uniq" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -3449,7 +3376,7 @@ dependencies = [ [[package]] name = "uu_unlink" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -3457,18 +3384,18 @@ dependencies = [ [[package]] name = "uu_uptime" -version = "0.0.28" +version = "0.0.29" dependencies = [ "chrono", "clap", - "thiserror", + "thiserror 2.0.11", "utmp-classic", "uucore", ] [[package]] name = "uu_users" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "utmp-classic", @@ -3477,7 +3404,7 @@ dependencies = [ [[package]] name = "uu_vdir" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uu_ls", @@ -3486,20 +3413,20 @@ dependencies = [ [[package]] name = "uu_wc" -version = "0.0.28" +version = "0.0.29" dependencies = [ "bytecount", "clap", "libc", "nix", - "thiserror", - "unicode-width 0.1.13", + "thiserror 2.0.11", + "unicode-width 0.2.0", "uucore", ] [[package]] name = "uu_who" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -3507,7 +3434,7 @@ dependencies = [ [[package]] name = "uu_whoami" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "libc", @@ -3517,17 +3444,17 @@ dependencies = [ [[package]] name = "uu_yes" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", - "itertools", + "itertools 0.14.0", "nix", "uucore", ] [[package]] name = "uucore" -version = "0.0.28" +version = "0.0.29" dependencies = [ "blake2b_simd", "blake3", @@ -3539,7 +3466,8 @@ dependencies = [ "dunce", "glob", "hex", - "itertools", + "itertools 0.14.0", + "lazy_static", "libc", "md-5", "memchr", @@ -3553,7 +3481,7 @@ dependencies = [ "sha3", "sm3", "tempfile", - "thiserror", + "thiserror 2.0.11", "time", "uucore_procs", "walkdir", @@ -3566,7 +3494,7 @@ dependencies = [ [[package]] name = "uucore_procs" -version = "0.0.28" +version = "0.0.29" dependencies = [ "proc-macro2", "quote", @@ -3575,13 +3503,13 @@ dependencies = [ [[package]] name = "uuhelp_parser" -version = "0.0.28" +version = "0.0.29" [[package]] name = "uuid" -version = "1.7.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a" +checksum = "b913a3b5fe84142e269d63cc62b64319ccaf89b748fc31fe025177f767a756c4" [[package]] name = "uutils_term_grid" @@ -3594,18 +3522,9 @@ dependencies = [ [[package]] name = "version_check" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - -[[package]] -name = "wait-timeout" -version = "0.2.0" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" -dependencies = [ - "libc", -] +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "walkdir" @@ -3625,34 +3544,34 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.87" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" dependencies = [ "cfg-if", + "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.87" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", - "syn 2.0.86", + "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.87" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3660,22 +3579,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.87" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.87" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" +checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" [[package]] name = "web-time" @@ -3746,15 +3665,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - [[package]] name = "windows-sys" version = "0.48.0" @@ -3782,21 +3692,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - [[package]] name = "windows-targets" version = "0.48.5" @@ -3828,12 +3723,6 @@ dependencies = [ "windows_x86_64_msvc 0.52.6", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -3846,12 +3735,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -3864,12 +3747,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -3888,12 +3765,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -3906,12 +3777,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -3924,12 +3789,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -3942,12 +3801,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -3962,9 +3815,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.20" +version = "0.6.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +checksum = "c8d71a593cc5c42ad7876e2c1fda56f314f3754c084128833e64f1345ff8a03a" dependencies = [ "memchr", ] @@ -3980,13 +3833,13 @@ dependencies = [ [[package]] name = "xattr" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" +checksum = "e105d177a3871454f754b33bb0ee637ecaaac997446375fd3e5d43a2ed00c909" dependencies = [ "libc", - "linux-raw-sys 0.4.14", - "rustix 0.38.37", + "linux-raw-sys 0.4.15", + "rustix 0.38.43", ] [[package]] @@ -4003,9 +3856,9 @@ checksum = "2a599daf1b507819c1121f0bf87fa37eb19daac6aff3aefefd4e6e2e0f2020fc" [[package]] name = "zerocopy" -version = "0.7.34" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ "byteorder", "zerocopy-derive", @@ -4013,20 +3866,20 @@ dependencies = [ [[package]] name = "zerocopy-derive" -version = "0.7.34" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn", ] [[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 +3887,21 @@ dependencies = [ "displaydoc", "flate2", "indexmap", - "num_enum", - "thiserror", + "memchr", + "thiserror 2.0.11", + "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 a6881abfb1a..ea87ccea79b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ [package] name = "coreutils" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "coreutils ~ GNU coreutils (updated); implemented as universal (cross-platform) utils, written in Rust" @@ -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" @@ -34,6 +34,8 @@ windows = ["feat_os_windows"] nightly = [] test_unimplemented = [] expensive_tests = [] +# "test_risky_names" == enable tests that create problematic file names (would make a network share inaccessible to Windows, breaks SVN on Mac OS, etc.) +test_risky_names = [] # * only build `uudoc` when `--feature uudoc` is activated uudoc = ["zip", "dep:uuhelp_parser"] ## features @@ -276,12 +278,13 @@ chrono = { version = "0.4.38", default-features = false, features = [ "alloc", "clock", ] } -clap = { version = "4.4", features = ["wrap_help", "cargo"] } +chrono-tz = "0.10.0" +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" @@ -295,8 +298,9 @@ gcd = "2.3" glob = "0.3.1" half = "2.4.1" hostname = "0.4" +iana-time-zone = "0.1.57" indicatif = "0.17.8" -itertools = "0.13.0" +itertools = "0.14.0" libc = "0.2.153" lscolors = { version = "0.20.0", default-features = false, features = [ "gnu_legacy", @@ -305,7 +309,7 @@ memchr = "2.7.2" memmap2 = "0.9.4" nix = { version = "0.29", default-features = false } nom = "7.1.3" -notify = { version = "=6.0.1", features = ["macos_kqueue"] } +notify = { version = "=8.0.0", features = ["macos_kqueue"] } num-bigint = "0.4.4" num-prime = "0.4.4" num-traits = "0.2.19" @@ -321,28 +325,28 @@ rand = { version = "0.8.5", features = ["small_rng"] } rand_core = "0.6.4" rayon = "1.10" regex = "1.10.4" -rstest = "0.23.0" +rstest = "0.24.0" rust-ini = "0.21.0" same-file = "1.0.6" self_cell = "1.0.4" selinux = "0.4.4" signal-hook = "0.3.17" smallvec = { version = "1.13.2", features = ["union"] } -tempfile = "3.10.1" +tempfile = "3.15.0" 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 +358,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.29", package = "uucore", path = "src/uucore" } +uucore_procs = { version = "0.0.29", package = "uucore_procs", path = "src/uucore_procs" } +uu_ls = { version = "0.0.29", path = "src/uu/ls" } +uu_base32 = { version = "0.0.29", path = "src/uu/base32" } [dependencies] clap = { workspace = true } @@ -373,109 +377,109 @@ zip = { workspace = true, optional = true } uuhelp_parser = { optional = true, version = ">=0.0.19", path = "src/uuhelp_parser" } # * uutils -uu_test = { optional = true, version = "0.0.28", package = "uu_test", path = "src/uu/test" } +uu_test = { optional = true, version = "0.0.29", package = "uu_test", path = "src/uu/test" } # -arch = { optional = true, version = "0.0.28", package = "uu_arch", path = "src/uu/arch" } -base32 = { optional = true, version = "0.0.28", package = "uu_base32", path = "src/uu/base32" } -base64 = { optional = true, version = "0.0.28", package = "uu_base64", path = "src/uu/base64" } -basename = { optional = true, version = "0.0.28", package = "uu_basename", path = "src/uu/basename" } -basenc = { optional = true, version = "0.0.28", package = "uu_basenc", path = "src/uu/basenc" } -cat = { optional = true, version = "0.0.28", package = "uu_cat", path = "src/uu/cat" } -chcon = { optional = true, version = "0.0.28", package = "uu_chcon", path = "src/uu/chcon" } -chgrp = { optional = true, version = "0.0.28", package = "uu_chgrp", path = "src/uu/chgrp" } -chmod = { optional = true, version = "0.0.28", package = "uu_chmod", path = "src/uu/chmod" } -chown = { optional = true, version = "0.0.28", package = "uu_chown", path = "src/uu/chown" } -chroot = { optional = true, version = "0.0.28", package = "uu_chroot", path = "src/uu/chroot" } -cksum = { optional = true, version = "0.0.28", package = "uu_cksum", path = "src/uu/cksum" } -comm = { optional = true, version = "0.0.28", package = "uu_comm", path = "src/uu/comm" } -cp = { optional = true, version = "0.0.28", package = "uu_cp", path = "src/uu/cp" } -csplit = { optional = true, version = "0.0.28", package = "uu_csplit", path = "src/uu/csplit" } -cut = { optional = true, version = "0.0.28", package = "uu_cut", path = "src/uu/cut" } -date = { optional = true, version = "0.0.28", package = "uu_date", path = "src/uu/date" } -dd = { optional = true, version = "0.0.28", package = "uu_dd", path = "src/uu/dd" } -df = { optional = true, version = "0.0.28", package = "uu_df", path = "src/uu/df" } -dir = { optional = true, version = "0.0.28", package = "uu_dir", path = "src/uu/dir" } -dircolors = { optional = true, version = "0.0.28", package = "uu_dircolors", path = "src/uu/dircolors" } -dirname = { optional = true, version = "0.0.28", package = "uu_dirname", path = "src/uu/dirname" } -du = { optional = true, version = "0.0.28", package = "uu_du", path = "src/uu/du" } -echo = { optional = true, version = "0.0.28", package = "uu_echo", path = "src/uu/echo" } -env = { optional = true, version = "0.0.28", package = "uu_env", path = "src/uu/env" } -expand = { optional = true, version = "0.0.28", package = "uu_expand", path = "src/uu/expand" } -expr = { optional = true, version = "0.0.28", package = "uu_expr", path = "src/uu/expr" } -factor = { optional = true, version = "0.0.28", package = "uu_factor", path = "src/uu/factor" } -false = { optional = true, version = "0.0.28", package = "uu_false", path = "src/uu/false" } -fmt = { optional = true, version = "0.0.28", package = "uu_fmt", path = "src/uu/fmt" } -fold = { optional = true, version = "0.0.28", package = "uu_fold", path = "src/uu/fold" } -groups = { optional = true, version = "0.0.28", package = "uu_groups", path = "src/uu/groups" } -hashsum = { optional = true, version = "0.0.28", package = "uu_hashsum", path = "src/uu/hashsum" } -head = { optional = true, version = "0.0.28", package = "uu_head", path = "src/uu/head" } -hostid = { optional = true, version = "0.0.28", package = "uu_hostid", path = "src/uu/hostid" } -hostname = { optional = true, version = "0.0.28", package = "uu_hostname", path = "src/uu/hostname" } -id = { optional = true, version = "0.0.28", package = "uu_id", path = "src/uu/id" } -install = { optional = true, version = "0.0.28", package = "uu_install", path = "src/uu/install" } -join = { optional = true, version = "0.0.28", package = "uu_join", path = "src/uu/join" } -kill = { optional = true, version = "0.0.28", package = "uu_kill", path = "src/uu/kill" } -link = { optional = true, version = "0.0.28", package = "uu_link", path = "src/uu/link" } -ln = { optional = true, version = "0.0.28", package = "uu_ln", path = "src/uu/ln" } -ls = { optional = true, version = "0.0.28", package = "uu_ls", path = "src/uu/ls" } -logname = { optional = true, version = "0.0.28", package = "uu_logname", path = "src/uu/logname" } -mkdir = { optional = true, version = "0.0.28", package = "uu_mkdir", path = "src/uu/mkdir" } -mkfifo = { optional = true, version = "0.0.28", package = "uu_mkfifo", path = "src/uu/mkfifo" } -mknod = { optional = true, version = "0.0.28", package = "uu_mknod", path = "src/uu/mknod" } -mktemp = { optional = true, version = "0.0.28", package = "uu_mktemp", path = "src/uu/mktemp" } -more = { optional = true, version = "0.0.28", package = "uu_more", path = "src/uu/more" } -mv = { optional = true, version = "0.0.28", package = "uu_mv", path = "src/uu/mv" } -nice = { optional = true, version = "0.0.28", package = "uu_nice", path = "src/uu/nice" } -nl = { optional = true, version = "0.0.28", package = "uu_nl", path = "src/uu/nl" } -nohup = { optional = true, version = "0.0.28", package = "uu_nohup", path = "src/uu/nohup" } -nproc = { optional = true, version = "0.0.28", package = "uu_nproc", path = "src/uu/nproc" } -numfmt = { optional = true, version = "0.0.28", package = "uu_numfmt", path = "src/uu/numfmt" } -od = { optional = true, version = "0.0.28", package = "uu_od", path = "src/uu/od" } -paste = { optional = true, version = "0.0.28", package = "uu_paste", path = "src/uu/paste" } -pathchk = { optional = true, version = "0.0.28", package = "uu_pathchk", path = "src/uu/pathchk" } -pinky = { optional = true, version = "0.0.28", package = "uu_pinky", path = "src/uu/pinky" } -pr = { optional = true, version = "0.0.28", package = "uu_pr", path = "src/uu/pr" } -printenv = { optional = true, version = "0.0.28", package = "uu_printenv", path = "src/uu/printenv" } -printf = { optional = true, version = "0.0.28", package = "uu_printf", path = "src/uu/printf" } -ptx = { optional = true, version = "0.0.28", package = "uu_ptx", path = "src/uu/ptx" } -pwd = { optional = true, version = "0.0.28", package = "uu_pwd", path = "src/uu/pwd" } -readlink = { optional = true, version = "0.0.28", package = "uu_readlink", path = "src/uu/readlink" } -realpath = { optional = true, version = "0.0.28", package = "uu_realpath", path = "src/uu/realpath" } -rm = { optional = true, version = "0.0.28", package = "uu_rm", path = "src/uu/rm" } -rmdir = { optional = true, version = "0.0.28", package = "uu_rmdir", path = "src/uu/rmdir" } -runcon = { optional = true, version = "0.0.28", package = "uu_runcon", path = "src/uu/runcon" } -seq = { optional = true, version = "0.0.28", package = "uu_seq", path = "src/uu/seq" } -shred = { optional = true, version = "0.0.28", package = "uu_shred", path = "src/uu/shred" } -shuf = { optional = true, version = "0.0.28", package = "uu_shuf", path = "src/uu/shuf" } -sleep = { optional = true, version = "0.0.28", package = "uu_sleep", path = "src/uu/sleep" } -sort = { optional = true, version = "0.0.28", package = "uu_sort", path = "src/uu/sort" } -split = { optional = true, version = "0.0.28", package = "uu_split", path = "src/uu/split" } -stat = { optional = true, version = "0.0.28", package = "uu_stat", path = "src/uu/stat" } -stdbuf = { optional = true, version = "0.0.28", package = "uu_stdbuf", path = "src/uu/stdbuf" } -stty = { optional = true, version = "0.0.28", package = "uu_stty", path = "src/uu/stty" } -sum = { optional = true, version = "0.0.28", package = "uu_sum", path = "src/uu/sum" } -sync = { optional = true, version = "0.0.28", package = "uu_sync", path = "src/uu/sync" } -tac = { optional = true, version = "0.0.28", package = "uu_tac", path = "src/uu/tac" } -tail = { optional = true, version = "0.0.28", package = "uu_tail", path = "src/uu/tail" } -tee = { optional = true, version = "0.0.28", package = "uu_tee", path = "src/uu/tee" } -timeout = { optional = true, version = "0.0.28", package = "uu_timeout", path = "src/uu/timeout" } -touch = { optional = true, version = "0.0.28", package = "uu_touch", path = "src/uu/touch" } -tr = { optional = true, version = "0.0.28", package = "uu_tr", path = "src/uu/tr" } -true = { optional = true, version = "0.0.28", package = "uu_true", path = "src/uu/true" } -truncate = { optional = true, version = "0.0.28", package = "uu_truncate", path = "src/uu/truncate" } -tsort = { optional = true, version = "0.0.28", package = "uu_tsort", path = "src/uu/tsort" } -tty = { optional = true, version = "0.0.28", package = "uu_tty", path = "src/uu/tty" } -uname = { optional = true, version = "0.0.28", package = "uu_uname", path = "src/uu/uname" } -unexpand = { optional = true, version = "0.0.28", package = "uu_unexpand", path = "src/uu/unexpand" } -uniq = { optional = true, version = "0.0.28", package = "uu_uniq", path = "src/uu/uniq" } -unlink = { optional = true, version = "0.0.28", package = "uu_unlink", path = "src/uu/unlink" } -uptime = { optional = true, version = "0.0.28", package = "uu_uptime", path = "src/uu/uptime" } -users = { optional = true, version = "0.0.28", package = "uu_users", path = "src/uu/users" } -vdir = { optional = true, version = "0.0.28", package = "uu_vdir", path = "src/uu/vdir" } -wc = { optional = true, version = "0.0.28", package = "uu_wc", path = "src/uu/wc" } -who = { optional = true, version = "0.0.28", package = "uu_who", path = "src/uu/who" } -whoami = { optional = true, version = "0.0.28", package = "uu_whoami", path = "src/uu/whoami" } -yes = { optional = true, version = "0.0.28", package = "uu_yes", path = "src/uu/yes" } +arch = { optional = true, version = "0.0.29", package = "uu_arch", path = "src/uu/arch" } +base32 = { optional = true, version = "0.0.29", package = "uu_base32", path = "src/uu/base32" } +base64 = { optional = true, version = "0.0.29", package = "uu_base64", path = "src/uu/base64" } +basename = { optional = true, version = "0.0.29", package = "uu_basename", path = "src/uu/basename" } +basenc = { optional = true, version = "0.0.29", package = "uu_basenc", path = "src/uu/basenc" } +cat = { optional = true, version = "0.0.29", package = "uu_cat", path = "src/uu/cat" } +chcon = { optional = true, version = "0.0.29", package = "uu_chcon", path = "src/uu/chcon" } +chgrp = { optional = true, version = "0.0.29", package = "uu_chgrp", path = "src/uu/chgrp" } +chmod = { optional = true, version = "0.0.29", package = "uu_chmod", path = "src/uu/chmod" } +chown = { optional = true, version = "0.0.29", package = "uu_chown", path = "src/uu/chown" } +chroot = { optional = true, version = "0.0.29", package = "uu_chroot", path = "src/uu/chroot" } +cksum = { optional = true, version = "0.0.29", package = "uu_cksum", path = "src/uu/cksum" } +comm = { optional = true, version = "0.0.29", package = "uu_comm", path = "src/uu/comm" } +cp = { optional = true, version = "0.0.29", package = "uu_cp", path = "src/uu/cp" } +csplit = { optional = true, version = "0.0.29", package = "uu_csplit", path = "src/uu/csplit" } +cut = { optional = true, version = "0.0.29", package = "uu_cut", path = "src/uu/cut" } +date = { optional = true, version = "0.0.29", package = "uu_date", path = "src/uu/date" } +dd = { optional = true, version = "0.0.29", package = "uu_dd", path = "src/uu/dd" } +df = { optional = true, version = "0.0.29", package = "uu_df", path = "src/uu/df" } +dir = { optional = true, version = "0.0.29", package = "uu_dir", path = "src/uu/dir" } +dircolors = { optional = true, version = "0.0.29", package = "uu_dircolors", path = "src/uu/dircolors" } +dirname = { optional = true, version = "0.0.29", package = "uu_dirname", path = "src/uu/dirname" } +du = { optional = true, version = "0.0.29", package = "uu_du", path = "src/uu/du" } +echo = { optional = true, version = "0.0.29", package = "uu_echo", path = "src/uu/echo" } +env = { optional = true, version = "0.0.29", package = "uu_env", path = "src/uu/env" } +expand = { optional = true, version = "0.0.29", package = "uu_expand", path = "src/uu/expand" } +expr = { optional = true, version = "0.0.29", package = "uu_expr", path = "src/uu/expr" } +factor = { optional = true, version = "0.0.29", package = "uu_factor", path = "src/uu/factor" } +false = { optional = true, version = "0.0.29", package = "uu_false", path = "src/uu/false" } +fmt = { optional = true, version = "0.0.29", package = "uu_fmt", path = "src/uu/fmt" } +fold = { optional = true, version = "0.0.29", package = "uu_fold", path = "src/uu/fold" } +groups = { optional = true, version = "0.0.29", package = "uu_groups", path = "src/uu/groups" } +hashsum = { optional = true, version = "0.0.29", package = "uu_hashsum", path = "src/uu/hashsum" } +head = { optional = true, version = "0.0.29", package = "uu_head", path = "src/uu/head" } +hostid = { optional = true, version = "0.0.29", package = "uu_hostid", path = "src/uu/hostid" } +hostname = { optional = true, version = "0.0.29", package = "uu_hostname", path = "src/uu/hostname" } +id = { optional = true, version = "0.0.29", package = "uu_id", path = "src/uu/id" } +install = { optional = true, version = "0.0.29", package = "uu_install", path = "src/uu/install" } +join = { optional = true, version = "0.0.29", package = "uu_join", path = "src/uu/join" } +kill = { optional = true, version = "0.0.29", package = "uu_kill", path = "src/uu/kill" } +link = { optional = true, version = "0.0.29", package = "uu_link", path = "src/uu/link" } +ln = { optional = true, version = "0.0.29", package = "uu_ln", path = "src/uu/ln" } +ls = { optional = true, version = "0.0.29", package = "uu_ls", path = "src/uu/ls" } +logname = { optional = true, version = "0.0.29", package = "uu_logname", path = "src/uu/logname" } +mkdir = { optional = true, version = "0.0.29", package = "uu_mkdir", path = "src/uu/mkdir" } +mkfifo = { optional = true, version = "0.0.29", package = "uu_mkfifo", path = "src/uu/mkfifo" } +mknod = { optional = true, version = "0.0.29", package = "uu_mknod", path = "src/uu/mknod" } +mktemp = { optional = true, version = "0.0.29", package = "uu_mktemp", path = "src/uu/mktemp" } +more = { optional = true, version = "0.0.29", package = "uu_more", path = "src/uu/more" } +mv = { optional = true, version = "0.0.29", package = "uu_mv", path = "src/uu/mv" } +nice = { optional = true, version = "0.0.29", package = "uu_nice", path = "src/uu/nice" } +nl = { optional = true, version = "0.0.29", package = "uu_nl", path = "src/uu/nl" } +nohup = { optional = true, version = "0.0.29", package = "uu_nohup", path = "src/uu/nohup" } +nproc = { optional = true, version = "0.0.29", package = "uu_nproc", path = "src/uu/nproc" } +numfmt = { optional = true, version = "0.0.29", package = "uu_numfmt", path = "src/uu/numfmt" } +od = { optional = true, version = "0.0.29", package = "uu_od", path = "src/uu/od" } +paste = { optional = true, version = "0.0.29", package = "uu_paste", path = "src/uu/paste" } +pathchk = { optional = true, version = "0.0.29", package = "uu_pathchk", path = "src/uu/pathchk" } +pinky = { optional = true, version = "0.0.29", package = "uu_pinky", path = "src/uu/pinky" } +pr = { optional = true, version = "0.0.29", package = "uu_pr", path = "src/uu/pr" } +printenv = { optional = true, version = "0.0.29", package = "uu_printenv", path = "src/uu/printenv" } +printf = { optional = true, version = "0.0.29", package = "uu_printf", path = "src/uu/printf" } +ptx = { optional = true, version = "0.0.29", package = "uu_ptx", path = "src/uu/ptx" } +pwd = { optional = true, version = "0.0.29", package = "uu_pwd", path = "src/uu/pwd" } +readlink = { optional = true, version = "0.0.29", package = "uu_readlink", path = "src/uu/readlink" } +realpath = { optional = true, version = "0.0.29", package = "uu_realpath", path = "src/uu/realpath" } +rm = { optional = true, version = "0.0.29", package = "uu_rm", path = "src/uu/rm" } +rmdir = { optional = true, version = "0.0.29", package = "uu_rmdir", path = "src/uu/rmdir" } +runcon = { optional = true, version = "0.0.29", package = "uu_runcon", path = "src/uu/runcon" } +seq = { optional = true, version = "0.0.29", package = "uu_seq", path = "src/uu/seq" } +shred = { optional = true, version = "0.0.29", package = "uu_shred", path = "src/uu/shred" } +shuf = { optional = true, version = "0.0.29", package = "uu_shuf", path = "src/uu/shuf" } +sleep = { optional = true, version = "0.0.29", package = "uu_sleep", path = "src/uu/sleep" } +sort = { optional = true, version = "0.0.29", package = "uu_sort", path = "src/uu/sort" } +split = { optional = true, version = "0.0.29", package = "uu_split", path = "src/uu/split" } +stat = { optional = true, version = "0.0.29", package = "uu_stat", path = "src/uu/stat" } +stdbuf = { optional = true, version = "0.0.29", package = "uu_stdbuf", path = "src/uu/stdbuf" } +stty = { optional = true, version = "0.0.29", package = "uu_stty", path = "src/uu/stty" } +sum = { optional = true, version = "0.0.29", package = "uu_sum", path = "src/uu/sum" } +sync = { optional = true, version = "0.0.29", package = "uu_sync", path = "src/uu/sync" } +tac = { optional = true, version = "0.0.29", package = "uu_tac", path = "src/uu/tac" } +tail = { optional = true, version = "0.0.29", package = "uu_tail", path = "src/uu/tail" } +tee = { optional = true, version = "0.0.29", package = "uu_tee", path = "src/uu/tee" } +timeout = { optional = true, version = "0.0.29", package = "uu_timeout", path = "src/uu/timeout" } +touch = { optional = true, version = "0.0.29", package = "uu_touch", path = "src/uu/touch" } +tr = { optional = true, version = "0.0.29", package = "uu_tr", path = "src/uu/tr" } +true = { optional = true, version = "0.0.29", package = "uu_true", path = "src/uu/true" } +truncate = { optional = true, version = "0.0.29", package = "uu_truncate", path = "src/uu/truncate" } +tsort = { optional = true, version = "0.0.29", package = "uu_tsort", path = "src/uu/tsort" } +tty = { optional = true, version = "0.0.29", package = "uu_tty", path = "src/uu/tty" } +uname = { optional = true, version = "0.0.29", package = "uu_uname", path = "src/uu/uname" } +unexpand = { optional = true, version = "0.0.29", package = "uu_unexpand", path = "src/uu/unexpand" } +uniq = { optional = true, version = "0.0.29", package = "uu_uniq", path = "src/uu/uniq" } +unlink = { optional = true, version = "0.0.29", package = "uu_unlink", path = "src/uu/unlink" } +uptime = { optional = true, version = "0.0.29", package = "uu_uptime", path = "src/uu/uptime" } +users = { optional = true, version = "0.0.29", package = "uu_users", path = "src/uu/users" } +vdir = { optional = true, version = "0.0.29", package = "uu_vdir", path = "src/uu/vdir" } +wc = { optional = true, version = "0.0.29", package = "uu_wc", path = "src/uu/wc" } +who = { optional = true, version = "0.0.29", package = "uu_who", path = "src/uu/who" } +whoami = { optional = true, version = "0.0.29", package = "uu_whoami", path = "src/uu/whoami" } +yes = { optional = true, version = "0.0.29", package = "uu_yes", path = "src/uu/yes" } # this breaks clippy linting with: "tests/by-util/test_factor_benches.rs: No such file or directory (os error 2)" # factor_benches = { optional = true, version = "0.0.0", package = "uu_factor_benches", path = "tests/benches/factor" } @@ -562,6 +566,8 @@ multiple_crate_versions = "allow" cargo_common_metadata = "allow" uninlined_format_args = "allow" missing_panics_doc = "allow" +# TODO remove when https://github.com/rust-lang/rust-clippy/issues/13774 is fixed +large_stack_arrays = "allow" use_self = "warn" needless_pass_by_value = "warn" diff --git a/GNUmakefile b/GNUmakefile index 0b4f2d04ce7..af73a10f4d0 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 22081c68939..37c5a596b3d 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/build.rs b/build.rs index 91e9d0427ce..d414de09209 100644 --- a/build.rs +++ b/build.rs @@ -33,7 +33,9 @@ pub fn main() { #[allow(clippy::match_same_arms)] match krate.as_ref() { "default" | "macos" | "unix" | "windows" | "selinux" | "zip" => continue, // common/standard feature names - "nightly" | "test_unimplemented" | "expensive_tests" => continue, // crate-local custom features + "nightly" | "test_unimplemented" | "expensive_tests" | "test_risky_names" => { + continue + } // crate-local custom features "uudoc" => continue, // is not a utility "test" => continue, // over-ridden with 'uu_test' to avoid collision with rust core crate 'test' s if s.starts_with(FEATURE_PREFIX) => continue, // crate feature sets diff --git a/deny.toml b/deny.toml index 9fefc77276e..26937bc653a 100644 --- a/deny.toml +++ b/deny.toml @@ -22,11 +22,12 @@ allow = [ "Apache-2.0", "ISC", "BSD-2-Clause", - "BSD-2-Clause-FreeBSD", "BSD-3-Clause", "BSL-1.0", "CC0-1.0", "Unicode-DFS-2016", + "Unicode-3.0", + "Zlib", ] confidence-threshold = 0.8 @@ -58,31 +59,13 @@ skip = [ { name = "linux-raw-sys", version = "0.3.8" }, # terminal_size { name = "rustix", version = "0.37.26" }, - # notify - { name = "windows-sys", version = "0.45.0" }, # various crates { name = "windows-sys", version = "0.48.0" }, # various crates { name = "windows-sys", version = "0.52.0" }, # windows-sys - { name = "windows-targets", version = "0.42.2" }, - # windows-sys { name = "windows-targets", version = "0.48.0" }, # windows-targets - { name = "windows_aarch64_gnullvm", version = "0.42.2" }, - # windows-targets - { name = "windows_aarch64_msvc", version = "0.42.2" }, - # windows-targets - { name = "windows_i686_gnu", version = "0.42.2" }, - # windows-targets - { name = "windows_i686_msvc", version = "0.42.2" }, - # windows-targets - { name = "windows_x86_64_gnu", version = "0.42.2" }, - # windows-targets - { name = "windows_x86_64_gnullvm", version = "0.42.2" }, - # windows-targets - { name = "windows_x86_64_msvc", version = "0.42.2" }, - # windows-targets { name = "windows_aarch64_gnullvm", version = "0.48.0" }, # windows-targets { name = "windows_aarch64_msvc", version = "0.48.0" }, @@ -104,6 +87,14 @@ skip = [ { name = "terminal_size", version = "0.2.6" }, # ansi-width, console, os_display { name = "unicode-width", version = "0.1.13" }, + # various crates + { name = "thiserror", version = "1.0.69" }, + # thiserror + { name = "thiserror-impl", version = "1.0.69" }, + # bindgen + { name = "itertools", version = "0.13.0" }, + # indexmap + { name = "hashbrown", version = "0.14.5" }, ] # spell-checker: enable diff --git a/docs/src/CODE_OF_CONDUCT.md b/docs/src/CODE_OF_CONDUCT.md new file mode 100644 index 00000000000..ce326a1ee03 --- /dev/null +++ b/docs/src/CODE_OF_CONDUCT.md @@ -0,0 +1,3 @@ + + +{{ #include ../../CODE_OF_CONDUCT.md }} diff --git a/docs/src/contributing.md b/docs/src/CONTRIBUTING.md similarity index 100% rename from docs/src/contributing.md rename to docs/src/CONTRIBUTING.md diff --git a/docs/src/DEVELOPMENT.md b/docs/src/DEVELOPMENT.md new file mode 100644 index 00000000000..580cecf0855 --- /dev/null +++ b/docs/src/DEVELOPMENT.md @@ -0,0 +1,3 @@ + + +{{ #include ../../DEVELOPMENT.md }} diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index 4f7c5608958..db31d38847d 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "aho-corasick" @@ -81,6 +81,18 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "autocfg" version = "1.3.0" @@ -89,9 +101,9 @@ checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "bigdecimal" -version = "0.4.3" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9324c8014cd04590682b34f1e9448d38f0674d0f7b2dc553331016ef0e4e9ebc" +checksum = "7f31f3af01c5c65a07985c804d3366560e6fa7883d640a122819b14ec327482c" dependencies = [ "autocfg", "libm", @@ -121,6 +133,39 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +[[package]] +name = "blake2b_simd" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23285ad32269793932e830392f2fe2f83e26488fd3ec778883a93c8323735780" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + +[[package]] +name = "blake3" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d82033247fd8e890df8f740e407ad4d038debb9eb1f40533fffb32e7d17dc6f7" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bstr" version = "1.9.1" @@ -146,13 +191,13 @@ checksum = "5ce89b21cab1437276d2650d57e971f9d548a2d9037cc231abdc0562b97498ce" [[package]] name = "cc" -version = "1.0.98" +version = "1.1.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f" +checksum = "40545c26d092346d8a8dab71ee48e7685a7a9cba76e634790c215b41a4a7b4cf" dependencies = [ "jobserver", "libc", - "once_cell", + "shlex", ] [[package]] @@ -185,6 +230,27 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "chrono-tz" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd6dd8046d00723a59a2f8c5f295c515b9bb9a331ee4f8f3d4dd49e428acd3b6" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf", +] + +[[package]] +name = "chrono-tz-build" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94fea34d77a245229e7746bd2beb786cd2a896f306ff491fb8cecb3074b10a7" +dependencies = [ + "parse-zoneinfo", + "phf_codegen", +] + [[package]] name = "clap" version = "4.5.4" @@ -245,12 +311,27 @@ dependencies = [ "tiny-keccak", ] +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + [[package]] name = "core-foundation-sys" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +[[package]] +name = "cpufeatures" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca741a962e1b0bff6d724a1a0958b686406e853bb14061f218562e1896f95e6" +dependencies = [ + "libc", +] + [[package]] name = "crossbeam-deque" version = "0.8.5" @@ -282,6 +363,16 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "ctrlc" version = "3.4.4" @@ -292,6 +383,42 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "data-encoding" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" + +[[package]] +name = "data-encoding-macro" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1559b6cba622276d6d63706db152618eeb15b89b3e4041446b05876e352e639" +dependencies = [ + "data-encoding", + "data-encoding-macro-internal", +] + +[[package]] +name = "data-encoding-macro-internal" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "332d754c0af53bc87c108fed664d121ecf59207ec4196041f04d6ab9002ad33f" +dependencies = [ + "data-encoding", + "syn 1.0.109", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "dlv-list" version = "0.5.2" @@ -335,6 +462,16 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -358,6 +495,12 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "iana-time-zone" version = "0.1.60" @@ -389,9 +532,9 @@ checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" [[package]] name = "itertools" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" dependencies = [ "either", ] @@ -414,21 +557,35 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "keccak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +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 = "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]] @@ -449,6 +606,16 @@ version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + [[package]] name = "memchr" version = "2.7.2" @@ -573,7 +740,16 @@ 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]] +name = "parse-zoneinfo" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24" +dependencies = [ + "regex", ] [[package]] @@ -587,6 +763,44 @@ dependencies = [ "regex", ] +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + [[package]] name = "pkg-config" version = "0.3.30" @@ -601,9 +815,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", ] @@ -709,9 +923,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", @@ -743,15 +957,68 @@ checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.89", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest", + "keccak", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "similar" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e" +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + +[[package]] +name = "sm3" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebb9a3b702d0a7e33bc4d85a14456633d2b165c2ad839c5fd9a8417c1ab15860" +dependencies = [ + "digest", +] + [[package]] name = "strsim" version = "0.11.1" @@ -760,9 +1027,20 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.65" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2863d96a84c6439701d7a38f9de935ec562c8832cc55d1dde0f513b52fad106" +checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e" dependencies = [ "proc-macro2", "quote", @@ -771,12 +1049,13 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.13.0" +version = "3.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" +checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" dependencies = [ "cfg-if", "fastrand", + "getrandom", "once_cell", "rustix", "windows-sys 0.59.0", @@ -794,22 +1073,22 @@ 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", - "syn", + "syn 2.0.89", ] [[package]] @@ -827,6 +1106,12 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc" +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + [[package]] name = "unicode-ident" version = "1.0.12" @@ -839,15 +1124,31 @@ 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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "uu_cksum" +version = "0.0.29" +dependencies = [ + "clap", + "hex", + "regex", + "uucore", +] + [[package]] name = "uu_cut" -version = "0.0.27" +version = "0.0.29" dependencies = [ "bstr", "clap", @@ -857,10 +1158,12 @@ dependencies = [ [[package]] name = "uu_date" -version = "0.0.27" +version = "0.0.29" dependencies = [ "chrono", + "chrono-tz", "clap", + "iana-time-zone", "libc", "parse_datetime", "uucore", @@ -869,7 +1172,7 @@ dependencies = [ [[package]] name = "uu_echo" -version = "0.0.27" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -877,7 +1180,7 @@ dependencies = [ [[package]] name = "uu_env" -version = "0.0.27" +version = "0.0.29" dependencies = [ "clap", "nix 0.29.0", @@ -887,7 +1190,7 @@ dependencies = [ [[package]] name = "uu_expr" -version = "0.0.27" +version = "0.0.29" dependencies = [ "clap", "num-bigint", @@ -898,7 +1201,7 @@ dependencies = [ [[package]] name = "uu_printf" -version = "0.0.27" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -906,7 +1209,7 @@ dependencies = [ [[package]] name = "uu_seq" -version = "0.0.27" +version = "0.0.29" dependencies = [ "bigdecimal", "clap", @@ -917,7 +1220,7 @@ dependencies = [ [[package]] name = "uu_sort" -version = "0.0.27" +version = "0.0.29" dependencies = [ "binary-heap-plus", "clap", @@ -931,13 +1234,13 @@ dependencies = [ "rayon", "self_cell", "tempfile", - "unicode-width", + "unicode-width 0.2.0", "uucore", ] [[package]] name = "uu_split" -version = "0.0.27" +version = "0.0.29" dependencies = [ "clap", "memchr", @@ -946,7 +1249,7 @@ dependencies = [ [[package]] name = "uu_test" -version = "0.0.27" +version = "0.0.29" dependencies = [ "clap", "libc", @@ -955,7 +1258,7 @@ dependencies = [ [[package]] name = "uu_tr" -version = "0.0.27" +version = "0.0.29" dependencies = [ "clap", "nom", @@ -964,34 +1267,50 @@ dependencies = [ [[package]] name = "uu_wc" -version = "0.0.27" +version = "0.0.29" dependencies = [ "bytecount", "clap", "libc", "nix 0.29.0", "thiserror", - "unicode-width", + "unicode-width 0.2.0", "uucore", ] [[package]] name = "uucore" -version = "0.0.27" +version = "0.0.29" dependencies = [ + "blake2b_simd", + "blake3", "clap", + "data-encoding", + "data-encoding-macro", + "digest", "dunce", "glob", + "hex", "itertools", + "lazy_static", "libc", + "md-5", + "memchr", "nix 0.29.0", "number_prefix", "once_cell", "os_display", + "regex", + "sha1", + "sha2", + "sha3", + "sm3", + "thiserror", "uucore_procs", "wild", "winapi-util", "windows-sys 0.59.0", + "z85", ] [[package]] @@ -1003,6 +1322,7 @@ dependencies = [ "rand", "similar", "tempfile", + "uu_cksum", "uu_cut", "uu_date", "uu_echo", @@ -1020,7 +1340,7 @@ dependencies = [ [[package]] name = "uucore_procs" -version = "0.0.27" +version = "0.0.29" dependencies = [ "proc-macro2", "quote", @@ -1029,7 +1349,13 @@ dependencies = [ [[package]] name = "uuhelp_parser" -version = "0.0.27" +version = "0.0.29" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wasi" @@ -1058,7 +1384,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.89", "wasm-bindgen-shared", ] @@ -1080,7 +1406,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.89", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -1265,3 +1591,9 @@ name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "z85" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a599daf1b507819c1121f0bf87fa37eb19daac6aff3aefefd4e6e2e0f2020fc" diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 3bc5a3433bc..190c57a51a6 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -10,7 +10,7 @@ cargo-fuzz = true [dependencies] libfuzzer-sys = "0.4.7" libc = "0.2.153" -tempfile = "3.11.0" +tempfile = "3.15.0" rand = { version = "0.8.5", features = ["small_rng"] } similar = "2.5.0" @@ -27,6 +27,7 @@ uu_cut = { path = "../src/uu/cut/" } uu_split = { path = "../src/uu/split/" } uu_tr = { path = "../src/uu/tr/" } uu_env = { path = "../src/uu/env/" } +uu_cksum = { path = "../src/uu/cksum/" } # Prevent this from interfering with workspaces [workspace] @@ -127,3 +128,9 @@ name = "fuzz_env" path = "fuzz_targets/fuzz_env.rs" test = false doc = false + +[[bin]] +name = "fuzz_cksum" +path = "fuzz_targets/fuzz_cksum.rs" +test = false +doc = false diff --git a/fuzz/fuzz_targets/fuzz_cksum.rs b/fuzz/fuzz_targets/fuzz_cksum.rs new file mode 100644 index 00000000000..411b21aab52 --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_cksum.rs @@ -0,0 +1,164 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. +// spell-checker:ignore chdir + +#![no_main] +use libfuzzer_sys::fuzz_target; +use std::ffi::OsString; +use uu_cksum::uumain; +mod fuzz_common; +use crate::fuzz_common::{ + compare_result, generate_and_run_uumain, generate_random_file, generate_random_string, + run_gnu_cmd, CommandResult, +}; +use rand::Rng; +use std::env::temp_dir; +use std::fs::{self, File}; +use std::io::Write; +use std::process::Command; + +static CMD_PATH: &str = "cksum"; + +fn generate_cksum_args() -> Vec { + let mut rng = rand::thread_rng(); + let mut args = Vec::new(); + + let digests = [ + "sysv", "bsd", "crc", "md5", "sha1", "sha224", "sha256", "sha384", "sha512", "blake2b", + "sm3", + ]; + let digest_opts = [ + "--base64", + "--raw", + "--tag", + "--untagged", + "--text", + "--binary", + ]; + + if rng.gen_bool(0.3) { + args.push("-a".to_string()); + args.push(digests[rng.gen_range(0..digests.len())].to_string()); + } + + if rng.gen_bool(0.2) { + args.push(digest_opts[rng.gen_range(0..digest_opts.len())].to_string()); + } + + if rng.gen_bool(0.15) { + args.push("-l".to_string()); + args.push(rng.gen_range(8..513).to_string()); + } + + if rng.gen_bool(0.05) { + for _ in 0..rng.gen_range(0..3) { + args.push(format!("file_{}", generate_random_string(5))); + } + } else { + args.push("-c".to_string()); + } + + if rng.gen_bool(0.25) { + if let Ok(file_path) = generate_random_file() { + args.push(file_path); + } + } + + if args.is_empty() || !args.iter().any(|arg| arg.starts_with("file_")) { + args.push("-a".to_string()); + args.push(digests[rng.gen_range(0..digests.len())].to_string()); + + if let Ok(file_path) = generate_random_file() { + args.push(file_path); + } + } + + args +} + +fn generate_checksum_file( + algo: &str, + file_path: &str, + digest_opts: &[&str], +) -> Result { + let checksum_file_path = temp_dir().join("checksum_file"); + let mut cmd = Command::new(CMD_PATH); + cmd.arg("-a").arg(algo); + + for opt in digest_opts { + cmd.arg(opt); + } + + cmd.arg(file_path); + let output = cmd.output()?; + + let mut checksum_file = File::create(&checksum_file_path)?; + checksum_file.write_all(&output.stdout)?; + + Ok(checksum_file_path.to_str().unwrap().to_string()) +} + +fn select_random_digest_opts<'a>( + rng: &mut rand::rngs::ThreadRng, + digest_opts: &'a [&'a str], +) -> Vec<&'a str> { + digest_opts + .iter() + .filter(|_| rng.gen_bool(0.5)) + .copied() + .collect() +} + +fuzz_target!(|_data: &[u8]| { + let cksum_args = generate_cksum_args(); + let mut args = vec![OsString::from("cksum")]; + args.extend(cksum_args.iter().map(OsString::from)); + + if let Ok(file_path) = generate_random_file() { + let algo = cksum_args + .iter() + .position(|arg| arg == "-a") + .map_or("md5", |index| &cksum_args[index + 1]); + + let all_digest_opts = ["--base64", "--raw", "--tag", "--untagged"]; + let mut rng = rand::thread_rng(); + let selected_digest_opts = select_random_digest_opts(&mut rng, &all_digest_opts); + + if let Ok(checksum_file_path) = + generate_checksum_file(algo, &file_path, &selected_digest_opts) + { + if let Ok(content) = fs::read_to_string(&checksum_file_path) { + println!("File content: {checksum_file_path}={content}"); + } else { + eprintln!("Error reading the checksum file."); + } + println!("args: {:?}", args); + let rust_result = generate_and_run_uumain(&args, uumain, None); + + let gnu_result = match run_gnu_cmd(CMD_PATH, &args[1..], false, None) { + Ok(result) => result, + Err(error_result) => { + eprintln!("Failed to run GNU command:"); + eprintln!("Stderr: {}", error_result.stderr); + eprintln!("Exit Code: {}", error_result.exit_code); + CommandResult { + stdout: String::new(), + stderr: error_result.stderr, + exit_code: error_result.exit_code, + } + } + }; + + compare_result( + "cksum", + &format!("{:?}", &args[1..]), + None, + &rust_result, + &gnu_result, + false, + ); + } + } +}); diff --git a/fuzz/fuzz_targets/fuzz_common.rs b/fuzz/fuzz_targets/fuzz_common.rs index 4cc04c8dc0b..f9d974cf779 100644 --- a/fuzz/fuzz_targets/fuzz_common.rs +++ b/fuzz/fuzz_targets/fuzz_common.rs @@ -8,7 +8,9 @@ use libc::{close, dup, dup2, pipe, STDERR_FILENO, STDOUT_FILENO}; use rand::prelude::SliceRandom; use rand::Rng; use similar::TextDiff; +use std::env::temp_dir; use std::ffi::OsString; +use std::fs::File; use std::io::{Seek, SeekFrom, Write}; use std::os::fd::{AsRawFd, RawFd}; use std::process::{Command, Stdio}; @@ -392,3 +394,23 @@ pub fn generate_random_string(max_length: usize) -> String { result } + +pub fn generate_random_file() -> Result { + let mut rng = rand::thread_rng(); + let file_name: String = (0..10) + .map(|_| rng.gen_range(b'a'..=b'z') as char) + .collect(); + let mut file_path = temp_dir(); + file_path.push(file_name); + + let mut file = File::create(&file_path)?; + + let content_length = rng.gen_range(10..1000); + let content: String = (0..content_length) + .map(|_| (rng.gen_range(b' '..=b'~') as char)) + .collect(); + + file.write_all(content.as_bytes())?; + + Ok(file_path.to_str().unwrap().to_string()) +} diff --git a/src/bin/uudoc.rs b/src/bin/uudoc.rs index 8ea11ed4d66..111e7a77fce 100644 --- a/src/bin/uudoc.rs +++ b/src/bin/uudoc.rs @@ -46,7 +46,9 @@ fn main() -> io::Result<()> { * [Installation](installation.md)\n\ * [Build from source](build.md)\n\ * [Platform support](platforms.md)\n\ - * [Contributing](contributing.md)\n\ + * [Contributing](CONTRIBUTING.md)\n\ + \t* [Development](DEVELOPMENT.md)\n\ + \t* [Code of Conduct](CODE_OF_CONDUCT.md)\n\ * [GNU test coverage](test_coverage.md)\n\ * [Extensions](extensions.md)\n\ \n\ @@ -172,7 +174,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/arch/Cargo.toml b/src/uu/arch/Cargo.toml index ccf2136f720..d1b8baea7d2 100644 --- a/src/uu/arch/Cargo.toml +++ b/src/uu/arch/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_arch" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "arch ~ (uutils) display machine architecture" diff --git a/src/uu/base32/Cargo.toml b/src/uu/base32/Cargo.toml index ffcd4796cea..b75a4cdc05b 100644 --- a/src/uu/base32/Cargo.toml +++ b/src/uu/base32/Cargo.toml @@ -1,8 +1,6 @@ -# spell-checker:ignore proptest - [package] name = "uu_base32" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "base32 ~ (uutils) decode/encode input (base32-encoding)" @@ -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 46a0361ea4a..e14e83921e2 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 f6b88f55157..878d07a92bb 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 0f2393c42ab..00000000000 --- 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/base64/Cargo.toml b/src/uu/base64/Cargo.toml index 7110b6395b0..4ed327ddc69 100644 --- a/src/uu/base64/Cargo.toml +++ b/src/uu/base64/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_base64" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "base64 ~ (uutils) decode/encode input (base64-encoding)" diff --git a/src/uu/basename/Cargo.toml b/src/uu/basename/Cargo.toml index 18f937b091f..31c962019d6 100644 --- a/src/uu/basename/Cargo.toml +++ b/src/uu/basename/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_basename" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "basename ~ (uutils) display PATHNAME with leading directory components removed" diff --git a/src/uu/basenc/Cargo.toml b/src/uu/basenc/Cargo.toml index d7a2849ca2b..a3bccb72c48 100644 --- a/src/uu/basenc/Cargo.toml +++ b/src/uu/basenc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_basenc" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "basenc ~ (uutils) decode/encode input" diff --git a/src/uu/cat/Cargo.toml b/src/uu/cat/Cargo.toml index f2df1c343ab..7a571c2cc95 100644 --- a/src/uu/cat/Cargo.toml +++ b/src/uu/cat/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_cat" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "cat ~ (uutils) concatenate and display input" diff --git a/src/uu/chcon/Cargo.toml b/src/uu/chcon/Cargo.toml index a4b914ad72a..897fafbe00f 100644 --- a/src/uu/chcon/Cargo.toml +++ b/src/uu/chcon/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_chcon" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "chcon ~ (uutils) change file security context" diff --git a/src/uu/chcon/src/chcon.rs b/src/uu/chcon/src/chcon.rs index 1a804bd3bbf..b5b892f6c36 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/chgrp/Cargo.toml b/src/uu/chgrp/Cargo.toml index 778576b887d..ec5e77ea569 100644 --- a/src/uu/chgrp/Cargo.toml +++ b/src/uu/chgrp/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_chgrp" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "chgrp ~ (uutils) change the group ownership of FILE" diff --git a/src/uu/chgrp/src/chgrp.rs b/src/uu/chgrp/src/chgrp.rs index fba2cef1611..fe5aee872e6 100644 --- a/src/uu/chgrp/src/chgrp.rs +++ b/src/uu/chgrp/src/chgrp.rs @@ -73,7 +73,7 @@ pub fn uu_app() -> Command { Arg::new(options::HELP) .long(options::HELP) .help("Print help information.") - .action(ArgAction::Help) + .action(ArgAction::Help), ) .arg( Arg::new(options::verbosity::CHANGES) @@ -101,20 +101,6 @@ pub fn uu_app() -> Command { .help("output a diagnostic for every file processed") .action(ArgAction::SetTrue), ) - .arg( - Arg::new(options::dereference::DEREFERENCE) - .long(options::dereference::DEREFERENCE) - .action(ArgAction::SetTrue), - ) - .arg( - Arg::new(options::dereference::NO_DEREFERENCE) - .short('h') - .long(options::dereference::NO_DEREFERENCE) - .help( - "affect symbolic links instead of any referenced file (useful only on systems that can change the ownership of a symlink)", - ) - .action(ArgAction::SetTrue), - ) .arg( Arg::new(options::preserve_root::PRESERVE) .long(options::preserve_root::PRESERVE) @@ -141,23 +127,6 @@ pub fn uu_app() -> Command { .help("operate on files and directories recursively") .action(ArgAction::SetTrue), ) - .arg( - Arg::new(options::traverse::TRAVERSE) - .short(options::traverse::TRAVERSE.chars().next().unwrap()) - .help("if a command line argument is a symbolic link to a directory, traverse it") - .action(ArgAction::SetTrue), - ) - .arg( - Arg::new(options::traverse::NO_TRAVERSE) - .short(options::traverse::NO_TRAVERSE.chars().next().unwrap()) - .help("do not traverse any symbolic links (default)") - .overrides_with_all([options::traverse::TRAVERSE, options::traverse::EVERY]) - .action(ArgAction::SetTrue), - ) - .arg( - Arg::new(options::traverse::EVERY) - .short(options::traverse::EVERY.chars().next().unwrap()) - .help("traverse every symbolic link to a directory encountered") - .action(ArgAction::SetTrue), - ) + // Add common arguments with chgrp, chown & chmod + .args(uucore::perms::common_args()) } diff --git a/src/uu/chmod/Cargo.toml b/src/uu/chmod/Cargo.toml index 9d62348792f..073abc76eaf 100644 --- a/src/uu/chmod/Cargo.toml +++ b/src/uu/chmod/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_chmod" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "chmod ~ (uutils) change mode of FILE" @@ -19,7 +19,7 @@ path = "src/chmod.rs" [dependencies] clap = { workspace = true } libc = { workspace = true } -uucore = { workspace = true, features = ["fs", "mode"] } +uucore = { workspace = true, features = ["fs", "mode", "perms"] } [[bin]] name = "chmod" diff --git a/src/uu/chmod/src/chmod.rs b/src/uu/chmod/src/chmod.rs index d1325743782..d2eb22ce6a6 100644 --- a/src/uu/chmod/src/chmod.rs +++ b/src/uu/chmod/src/chmod.rs @@ -16,6 +16,7 @@ use uucore::fs::display_permissions_unix; use uucore::libc::mode_t; #[cfg(not(windows))] use uucore::mode; +use uucore::perms::{configure_symlink_and_recursion, TraverseSymlinks}; use uucore::{format_usage, help_about, help_section, help_usage, show, show_error}; const ABOUT: &str = help_about!("chmod.md"); @@ -23,6 +24,7 @@ const USAGE: &str = help_usage!("chmod.md"); const LONG_USAGE: &str = help_section!("after help", "chmod.md"); mod options { + pub const HELP: &str = "help"; pub const CHANGES: &str = "changes"; pub const QUIET: &str = "quiet"; // visible_alias("silent") pub const VERBOSE: &str = "verbose"; @@ -98,7 +100,6 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let quiet = matches.get_flag(options::QUIET); let verbose = matches.get_flag(options::VERBOSE); let preserve_root = matches.get_flag(options::PRESERVE_ROOT); - let recursive = matches.get_flag(options::RECURSIVE); let fmode = match matches.get_one::(options::REFERENCE) { Some(fref) => match fs::metadata(fref) { Ok(meta) => Some(meta.mode() & 0o7777), @@ -137,6 +138,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { return Err(UUsageError::new(1, "missing operand".to_string())); } + let (recursive, dereference, traverse_symlinks) = configure_symlink_and_recursion(&matches)?; + let chmoder = Chmoder { changes, quiet, @@ -145,6 +148,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { recursive, fmode, cmode, + traverse_symlinks, + dereference, }; chmoder.chmod(&files) @@ -158,6 +163,13 @@ pub fn uu_app() -> Command { .args_override_self(true) .infer_long_args(true) .no_binary_name(true) + .disable_help_flag(true) + .arg( + Arg::new(options::HELP) + .long(options::HELP) + .help("Print help information.") + .action(ArgAction::Help), + ) .arg( Arg::new(options::CHANGES) .long(options::CHANGES) @@ -206,9 +218,10 @@ pub fn uu_app() -> Command { .help("use RFILE's mode instead of MODE values"), ) .arg( - Arg::new(options::MODE).required_unless_present(options::REFERENCE), // It would be nice if clap could parse with delimiter, e.g. "g-x,u+x", - // however .multiple_occurrences(true) cannot be used here because FILE already needs that. - // Only one positional argument with .multiple_occurrences(true) set is allowed per command + Arg::new(options::MODE).required_unless_present(options::REFERENCE), + // It would be nice if clap could parse with delimiter, e.g. "g-x,u+x", + // however .multiple_occurrences(true) cannot be used here because FILE already needs that. + // Only one positional argument with .multiple_occurrences(true) set is allowed per command ) .arg( Arg::new(options::FILE) @@ -216,6 +229,8 @@ pub fn uu_app() -> Command { .action(ArgAction::Append) .value_hint(clap::ValueHint::AnyPath), ) + // Add common arguments with chgrp, chown & chmod + .args(uucore::perms::common_args()) } struct Chmoder { @@ -226,6 +241,8 @@ struct Chmoder { recursive: bool, fmode: Option, cmode: Option, + traverse_symlinks: TraverseSymlinks, + dereference: bool, } impl Chmoder { @@ -237,12 +254,19 @@ impl Chmoder { let file = Path::new(filename); if !file.exists() { if file.is_symlink() { + if !self.dereference && !self.recursive { + // The file is a symlink and we should not follow it + // Don't try to change the mode of the symlink itself + continue; + } if !self.quiet { show!(USimpleError::new( 1, format!("cannot operate on dangling symlink {}", filename.quote()), )); + set_exit_code(1); } + if self.verbose { println!( "failed to change mode of {} from 0000 (---------) to 1500 (r-x-----T)", @@ -262,6 +286,11 @@ impl Chmoder { // So we set the exit code, because it hasn't been set yet if `self.quiet` is true. set_exit_code(1); continue; + } else if !self.dereference && file.is_symlink() { + // The file is a symlink and we should not follow it + // chmod 755 --no-dereference a/link + // should not change the permissions in this case + continue; } if self.recursive && self.preserve_root && filename == "/" { return Err(USimpleError::new( @@ -283,11 +312,23 @@ impl Chmoder { fn walk_dir(&self, file_path: &Path) -> UResult<()> { let mut r = self.chmod_file(file_path); - if !file_path.is_symlink() && file_path.is_dir() { + // Determine whether to traverse symlinks based on `self.traverse_symlinks` + let should_follow_symlink = match self.traverse_symlinks { + TraverseSymlinks::All => true, + TraverseSymlinks::First => { + file_path == file_path.canonicalize().unwrap_or(file_path.to_path_buf()) + } + TraverseSymlinks::None => false, + }; + + // If the path is a directory (or we should follow symlinks), recurse into it + if (!file_path.is_symlink() || should_follow_symlink) && file_path.is_dir() { for dir_entry in file_path.read_dir()? { let path = dir_entry?.path(); if !path.is_symlink() { r = self.walk_dir(path.as_path()); + } else if should_follow_symlink { + r = self.chmod_file(path.as_path()).and(r); } } } @@ -303,19 +344,22 @@ impl Chmoder { } #[cfg(unix)] fn chmod_file(&self, file: &Path) -> UResult<()> { - use uucore::mode::get_umask; + use uucore::{mode::get_umask, perms::get_metadata}; - let fperm = match fs::metadata(file) { + let metadata = get_metadata(file, self.dereference); + + let fperm = match metadata { Ok(meta) => meta.mode() & 0o7777, Err(err) => { - if file.is_symlink() { + // Handle dangling symlinks or other errors + if file.is_symlink() && !self.dereference { if self.verbose { println!( "neither symbolic link {} nor referent has been changed", file.quote() ); } - return Ok(()); + return Ok(()); // Skip dangling symlinks } else if err.kind() == std::io::ErrorKind::PermissionDenied { // These two filenames would normally be conditionally // quoted, but GNU's tests expect them to always be quoted @@ -328,6 +372,8 @@ impl Chmoder { } } }; + + // Determine the new permissions to apply match self.fmode { Some(mode) => self.change_file(fperm, mode, file)?, None => { diff --git a/src/uu/chown/Cargo.toml b/src/uu/chown/Cargo.toml index 0312d84b2a1..be05584b38e 100644 --- a/src/uu/chown/Cargo.toml +++ b/src/uu/chown/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_chown" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "chown ~ (uutils) change the ownership of FILE" diff --git a/src/uu/chown/src/chown.rs b/src/uu/chown/src/chown.rs index 0e9b8b2423c..20bc87c341d 100644 --- a/src/uu/chown/src/chown.rs +++ b/src/uu/chown/src/chown.rs @@ -96,25 +96,6 @@ pub fn uu_app() -> Command { .help("like verbose but report only when a change is made") .action(ArgAction::SetTrue), ) - .arg( - Arg::new(options::dereference::DEREFERENCE) - .long(options::dereference::DEREFERENCE) - .help( - "affect the referent of each symbolic link (this is the default), \ - rather than the symbolic link itself", - ) - .action(ArgAction::SetTrue), - ) - .arg( - Arg::new(options::dereference::NO_DEREFERENCE) - .short('h') - .long(options::dereference::NO_DEREFERENCE) - .help( - "affect symbolic links instead of any referenced file \ - (useful only on systems that can change the ownership of a symlink)", - ) - .action(ArgAction::SetTrue), - ) .arg( Arg::new(options::FROM) .long(options::FROM) @@ -165,27 +146,6 @@ pub fn uu_app() -> Command { .long(options::verbosity::SILENT) .action(ArgAction::SetTrue), ) - .arg( - Arg::new(options::traverse::TRAVERSE) - .short(options::traverse::TRAVERSE.chars().next().unwrap()) - .help("if a command line argument is a symbolic link to a directory, traverse it") - .overrides_with_all([options::traverse::EVERY, options::traverse::NO_TRAVERSE]) - .action(ArgAction::SetTrue), - ) - .arg( - Arg::new(options::traverse::EVERY) - .short(options::traverse::EVERY.chars().next().unwrap()) - .help("traverse every symbolic link to a directory encountered") - .overrides_with_all([options::traverse::TRAVERSE, options::traverse::NO_TRAVERSE]) - .action(ArgAction::SetTrue), - ) - .arg( - Arg::new(options::traverse::NO_TRAVERSE) - .short(options::traverse::NO_TRAVERSE.chars().next().unwrap()) - .help("do not traverse any symbolic links (default)") - .overrides_with_all([options::traverse::TRAVERSE, options::traverse::EVERY]) - .action(ArgAction::SetTrue), - ) .arg( Arg::new(options::verbosity::VERBOSE) .long(options::verbosity::VERBOSE) @@ -193,6 +153,8 @@ pub fn uu_app() -> Command { .help("output a diagnostic for every file processed") .action(ArgAction::SetTrue), ) + // Add common arguments with chgrp, chown & chmod + .args(uucore::perms::common_args()) } /// Parses the user string to extract the UID. diff --git a/src/uu/chroot/Cargo.toml b/src/uu/chroot/Cargo.toml index 797b6246026..9a9a8290fdf 100644 --- a/src/uu/chroot/Cargo.toml +++ b/src/uu/chroot/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_chroot" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "chroot ~ (uutils) run COMMAND under a new root directory" diff --git a/src/uu/chroot/src/chroot.rs b/src/uu/chroot/src/chroot.rs index fb20b0ccc46..4ea5db65348 100644 --- a/src/uu/chroot/src/chroot.rs +++ b/src/uu/chroot/src/chroot.rs @@ -11,26 +11,152 @@ use clap::{crate_version, Arg, ArgAction, Command}; use std::ffi::CString; use std::io::Error; use std::os::unix::prelude::OsStrExt; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::process; +use uucore::entries::{grp2gid, usr2uid, Locate, Passwd}; use uucore::error::{set_exit_code, UClapError, UResult, UUsageError}; use uucore::fs::{canonicalize, MissingHandling, ResolveMode}; use uucore::libc::{self, chroot, setgid, setgroups, setuid}; -use uucore::{entries, format_usage, help_about, help_usage}; +use uucore::{format_usage, help_about, help_usage, show}; static ABOUT: &str = help_about!("chroot.md"); static USAGE: &str = help_usage!("chroot.md"); mod options { pub const NEWROOT: &str = "newroot"; - pub const USER: &str = "user"; - pub const GROUP: &str = "group"; pub const GROUPS: &str = "groups"; pub const USERSPEC: &str = "userspec"; pub const COMMAND: &str = "command"; pub const SKIP_CHDIR: &str = "skip-chdir"; } +/// A user and group specification, where each is optional. +enum UserSpec { + NeitherGroupNorUser, + UserOnly(String), + GroupOnly(String), + UserAndGroup(String, String), +} + +struct Options { + /// Path to the new root directory. + newroot: PathBuf, + /// Whether to change to the new root directory. + skip_chdir: bool, + /// List of groups under which the command will be run. + groups: Option>, + /// The user and group (each optional) under which the command will be run. + userspec: Option, +} + +/// Parse a user and group from the argument to `--userspec`. +/// +/// The `spec` must be of the form `[USER][:[GROUP]]`, otherwise an +/// error is returned. +fn parse_userspec(spec: &str) -> UResult { + match &spec.splitn(2, ':').collect::>()[..] { + // "" + [""] => Ok(UserSpec::NeitherGroupNorUser), + // "usr" + [usr] => Ok(UserSpec::UserOnly(usr.to_string())), + // ":" + ["", ""] => Ok(UserSpec::NeitherGroupNorUser), + // ":grp" + ["", grp] => Ok(UserSpec::GroupOnly(grp.to_string())), + // "usr:" + [usr, ""] => Ok(UserSpec::UserOnly(usr.to_string())), + // "usr:grp" + [usr, grp] => Ok(UserSpec::UserAndGroup(usr.to_string(), grp.to_string())), + // everything else + _ => Err(ChrootError::InvalidUserspec(spec.to_string()).into()), + } +} + +// Pre-condition: `list_str` is non-empty. +fn parse_group_list(list_str: &str) -> Result, ChrootError> { + let split: Vec<&str> = list_str.split(",").collect(); + if split.len() == 1 { + let name = split[0].trim(); + if name.is_empty() { + // --groups=" " + // chroot: invalid group ‘ ’ + Err(ChrootError::InvalidGroup(name.to_string())) + } else { + // --groups="blah" + Ok(vec![name.to_string()]) + } + } else if split.iter().all(|s| s.is_empty()) { + // --groups="," + // chroot: invalid group list ‘,’ + Err(ChrootError::InvalidGroupList(list_str.to_string())) + } else { + let mut result = vec![]; + let mut err = false; + for name in split { + let trimmed_name = name.trim(); + if trimmed_name.is_empty() { + if name.is_empty() { + // --groups="," + continue; + } else { + // --groups=", " + // chroot: invalid group ‘ ’ + show!(ChrootError::InvalidGroup(name.to_string())); + err = true; + } + } else { + // TODO Figure out a better condition here. + if trimmed_name.starts_with(char::is_numeric) + && trimmed_name.ends_with(|c: char| !c.is_numeric()) + { + // --groups="0trail" + // chroot: invalid group ‘0trail’ + show!(ChrootError::InvalidGroup(name.to_string())); + err = true; + } else { + result.push(trimmed_name.to_string()); + } + } + } + if err { + Err(ChrootError::GroupsParsingFailed) + } else { + Ok(result) + } + } +} + +impl Options { + /// Parse parameters from the command-line arguments. + fn from(matches: &clap::ArgMatches) -> UResult { + let newroot = match matches.get_one::(options::NEWROOT) { + Some(v) => Path::new(v).to_path_buf(), + None => return Err(ChrootError::MissingNewRoot.into()), + }; + let groups = match matches.get_one::(options::GROUPS) { + None => None, + Some(s) => { + if s.is_empty() { + Some(vec![]) + } else { + Some(parse_group_list(s)?) + } + } + }; + let skip_chdir = matches.get_flag(options::SKIP_CHDIR); + let userspec = match matches.get_one::(options::USERSPEC) { + None => None, + Some(s) => Some(parse_userspec(s)?), + }; + Ok(Self { + newroot, + skip_chdir, + groups, + userspec, + }) + } +} + #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uu_app().try_get_matches_from(args).with_exit_code(125)?; @@ -39,17 +165,17 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let default_option: &'static str = "-i"; let user_shell = std::env::var("SHELL"); - let newroot: &Path = match matches.get_one::(options::NEWROOT) { - Some(v) => Path::new(v), - None => return Err(ChrootError::MissingNewRoot.into()), - }; + let options = Options::from(&matches)?; - let skip_chdir = matches.get_flag(options::SKIP_CHDIR); // We are resolving the path in case it is a symlink or /. or /../ - if skip_chdir - && canonicalize(newroot, MissingHandling::Normal, ResolveMode::Logical) - .unwrap() - .to_str() + if options.skip_chdir + && canonicalize( + &options.newroot, + MissingHandling::Normal, + ResolveMode::Logical, + ) + .unwrap() + .to_str() != Some("/") { return Err(UUsageError::new( @@ -58,8 +184,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { )); } - if !newroot.is_dir() { - return Err(ChrootError::NoSuchDirectory(format!("{}", newroot.display())).into()); + if !options.newroot.is_dir() { + return Err(ChrootError::NoSuchDirectory(format!("{}", options.newroot.display())).into()); } let commands = match matches.get_many::(options::COMMAND) { @@ -85,7 +211,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let chroot_args = &command[1..]; // NOTE: Tests can only trigger code beyond this point if they're invoked with root permissions - set_context(newroot, &matches)?; + set_context(&options)?; let pstatus = match process::Command::new(chroot_command) .args(chroot_args) @@ -125,35 +251,17 @@ pub fn uu_app() -> Command { .required(true) .index(1), ) - .arg( - Arg::new(options::USER) - .short('u') - .long(options::USER) - .help("User (ID or name) to switch before running the program") - .value_name("USER"), - ) - .arg( - Arg::new(options::GROUP) - .short('g') - .long(options::GROUP) - .help("Group (ID or name) to switch to") - .value_name("GROUP"), - ) .arg( Arg::new(options::GROUPS) - .short('G') .long(options::GROUPS) + .overrides_with(options::GROUPS) .help("Comma-separated list of groups to switch to") .value_name("GROUP1,GROUP2..."), ) .arg( Arg::new(options::USERSPEC) .long(options::USERSPEC) - .help( - "Colon-separated user and group to switch to. \ - Same as -u USER -g GROUP. \ - Userspec has higher preference than -u and/or -g", - ) + .help("Colon-separated user and group to switch to.") .value_name("USER:GROUP"), ) .arg( @@ -175,119 +283,177 @@ pub fn uu_app() -> Command { ) } -fn set_context(root: &Path, options: &clap::ArgMatches) -> UResult<()> { - let userspec_str = options.get_one::(options::USERSPEC); - let user_str = options - .get_one::(options::USER) - .map(|s| s.as_str()) - .unwrap_or_default(); - let group_str = options - .get_one::(options::GROUP) - .map(|s| s.as_str()) - .unwrap_or_default(); - let groups_str = options - .get_one::(options::GROUPS) - .map(|s| s.as_str()) - .unwrap_or_default(); - let skip_chdir = options.contains_id(options::SKIP_CHDIR); - let userspec = match userspec_str { - Some(u) => { - let s: Vec<&str> = u.split(':').collect(); - if s.len() != 2 || s.iter().any(|&spec| spec.is_empty()) { - return Err(ChrootError::InvalidUserspec(u.to_string()).into()); - }; - s - } - None => Vec::new(), - }; - - let (user, group) = if userspec.is_empty() { - (user_str, group_str) - } else { - (userspec[0], userspec[1]) - }; +/// Get the UID for the given username, falling back to numeric parsing. +/// +/// According to the documentation of GNU `chroot`, "POSIX requires that +/// these commands first attempt to resolve the specified string as a +/// name, and only once that fails, then try to interpret it as an ID." +fn name_to_uid(name: &str) -> Result { + match usr2uid(name) { + Ok(uid) => Ok(uid), + Err(_) => name + .parse::() + .map_err(|_| ChrootError::NoSuchUser), + } +} - enter_chroot(root, skip_chdir)?; +/// Get the GID for the given group name, falling back to numeric parsing. +/// +/// According to the documentation of GNU `chroot`, "POSIX requires that +/// these commands first attempt to resolve the specified string as a +/// name, and only once that fails, then try to interpret it as an ID." +fn name_to_gid(name: &str) -> Result { + match grp2gid(name) { + Ok(gid) => Ok(gid), + Err(_) => name + .parse::() + .map_err(|_| ChrootError::NoSuchGroup), + } +} - set_groups_from_str(groups_str)?; - set_main_group(group)?; - set_user(user)?; - Ok(()) +/// Get the list of group IDs for the given user. +/// +/// According to the GNU documentation, "the supplementary groups are +/// set according to the system defined list for that user". This +/// function gets that list. +fn supplemental_gids(uid: libc::uid_t) -> Vec { + match Passwd::locate(uid) { + Err(_) => vec![], + Ok(passwd) => passwd.belongs_to(), + } } -fn enter_chroot(root: &Path, skip_chdir: bool) -> UResult<()> { - let err = unsafe { - chroot( - CString::new(root.as_os_str().as_bytes().to_vec()) - .unwrap() - .as_bytes_with_nul() - .as_ptr() as *const libc::c_char, - ) - }; +/// Set the supplemental group IDs for this process. +fn set_supplemental_gids(gids: &[libc::gid_t]) -> std::io::Result<()> { + #[cfg(any(target_vendor = "apple", target_os = "freebsd", target_os = "openbsd"))] + let n = gids.len() as libc::c_int; + #[cfg(any(target_os = "linux", target_os = "android"))] + let n = gids.len() as libc::size_t; + let err = unsafe { setgroups(n, gids.as_ptr()) }; + if err == 0 { + Ok(()) + } else { + Err(Error::last_os_error()) + } +} +/// Set the group ID of this process. +fn set_gid(gid: libc::gid_t) -> std::io::Result<()> { + let err = unsafe { setgid(gid) }; if err == 0 { - if !skip_chdir { - std::env::set_current_dir(root).unwrap(); - } Ok(()) } else { - Err(ChrootError::CannotEnter(format!("{}", root.display()), Error::last_os_error()).into()) + Err(Error::last_os_error()) } } -fn set_main_group(group: &str) -> UResult<()> { - if !group.is_empty() { - let group_id = match entries::grp2gid(group) { - Ok(g) => g, - _ => return Err(ChrootError::NoSuchGroup(group.to_string()).into()), - }; - let err = unsafe { setgid(group_id) }; - if err != 0 { - return Err( - ChrootError::SetGidFailed(group_id.to_string(), Error::last_os_error()).into(), - ); - } +/// Set the user ID of this process. +fn set_uid(uid: libc::uid_t) -> std::io::Result<()> { + let err = unsafe { setuid(uid) }; + if err == 0 { + Ok(()) + } else { + Err(Error::last_os_error()) } - Ok(()) } -#[cfg(any(target_vendor = "apple", target_os = "freebsd", target_os = "openbsd"))] -fn set_groups(groups: &[libc::gid_t]) -> libc::c_int { - unsafe { setgroups(groups.len() as libc::c_int, groups.as_ptr()) } +/// What to do when the `--groups` argument is missing. +enum Strategy { + /// Do nothing. + Nothing, + /// Use the list of supplemental groups for the given user. + /// + /// If the `bool` parameter is `false` and the list of groups for + /// the given user is empty, then this will result in an error. + FromUID(libc::uid_t, bool), } -#[cfg(any(target_os = "linux", target_os = "android"))] -fn set_groups(groups: &[libc::gid_t]) -> libc::c_int { - unsafe { setgroups(groups.len() as libc::size_t, groups.as_ptr()) } +/// Set supplemental groups when the `--groups` argument is not specified. +fn handle_missing_groups(strategy: Strategy) -> Result<(), ChrootError> { + match strategy { + Strategy::Nothing => Ok(()), + Strategy::FromUID(uid, false) => { + let gids = supplemental_gids(uid); + if gids.is_empty() { + Err(ChrootError::NoGroupSpecified(uid)) + } else { + set_supplemental_gids(&gids).map_err(ChrootError::SetGroupsFailed) + } + } + Strategy::FromUID(uid, true) => { + let gids = supplemental_gids(uid); + set_supplemental_gids(&gids).map_err(ChrootError::SetGroupsFailed) + } + } } -fn set_groups_from_str(groups: &str) -> UResult<()> { - if !groups.is_empty() { - let mut groups_vec = vec![]; - for group in groups.split(',') { - let gid = match entries::grp2gid(group) { - Ok(g) => g, - Err(_) => return Err(ChrootError::NoSuchGroup(group.to_string()).into()), - }; - groups_vec.push(gid); +/// Set supplemental groups for this process. +fn set_supplemental_gids_with_strategy( + strategy: Strategy, + groups: &Option>, +) -> Result<(), ChrootError> { + match groups { + None => handle_missing_groups(strategy), + Some(groups) => { + let mut gids = vec![]; + for group in groups { + gids.push(name_to_gid(group)?); + } + set_supplemental_gids(&gids).map_err(ChrootError::SetGroupsFailed) + } + } +} + +/// Change the root, set the user ID, and set the group IDs for this process. +fn set_context(options: &Options) -> UResult<()> { + enter_chroot(&options.newroot, options.skip_chdir)?; + match &options.userspec { + None | Some(UserSpec::NeitherGroupNorUser) => { + let strategy = Strategy::Nothing; + set_supplemental_gids_with_strategy(strategy, &options.groups)?; + } + Some(UserSpec::UserOnly(user)) => { + let uid = name_to_uid(user)?; + let gid = uid as libc::gid_t; + let strategy = Strategy::FromUID(uid, false); + set_supplemental_gids_with_strategy(strategy, &options.groups)?; + set_gid(gid).map_err(|e| ChrootError::SetGidFailed(user.to_string(), e))?; + set_uid(uid).map_err(|e| ChrootError::SetUserFailed(user.to_string(), e))?; } - let err = set_groups(&groups_vec); - if err != 0 { - return Err(ChrootError::SetGroupsFailed(Error::last_os_error()).into()); + Some(UserSpec::GroupOnly(group)) => { + let gid = name_to_gid(group)?; + let strategy = Strategy::Nothing; + set_supplemental_gids_with_strategy(strategy, &options.groups)?; + set_gid(gid).map_err(|e| ChrootError::SetGidFailed(group.to_string(), e))?; + } + Some(UserSpec::UserAndGroup(user, group)) => { + let uid = name_to_uid(user)?; + let gid = name_to_gid(group)?; + let strategy = Strategy::FromUID(uid, true); + set_supplemental_gids_with_strategy(strategy, &options.groups)?; + set_gid(gid).map_err(|e| ChrootError::SetGidFailed(group.to_string(), e))?; + set_uid(uid).map_err(|e| ChrootError::SetUserFailed(user.to_string(), e))?; } } Ok(()) } -fn set_user(user: &str) -> UResult<()> { - if !user.is_empty() { - let user_id = entries::usr2uid(user).unwrap(); - let err = unsafe { setuid(user_id as libc::uid_t) }; - if err != 0 { - return Err( - ChrootError::SetUserFailed(user.to_string(), Error::last_os_error()).into(), - ); +fn enter_chroot(root: &Path, skip_chdir: bool) -> UResult<()> { + let err = unsafe { + chroot( + CString::new(root.as_os_str().as_bytes().to_vec()) + .unwrap() + .as_bytes_with_nul() + .as_ptr() as *const libc::c_char, + ) + }; + + if err == 0 { + if !skip_chdir { + std::env::set_current_dir(root).unwrap(); } + Ok(()) + } else { + Err(ChrootError::CannotEnter(format!("{}", root.display()), Error::last_os_error()).into()) } - Ok(()) } diff --git a/src/uu/chroot/src/error.rs b/src/uu/chroot/src/error.rs index 526f1a75a43..b8109d41910 100644 --- a/src/uu/chroot/src/error.rs +++ b/src/uu/chroot/src/error.rs @@ -8,6 +8,7 @@ use std::fmt::Display; use std::io::Error; use uucore::display::Quotable; use uucore::error::UError; +use uucore::libc; /// Errors that can happen while executing chroot. #[derive(Debug)] @@ -21,14 +22,25 @@ pub enum ChrootError { /// Failed to find the specified command. CommandNotFound(String, Error), + GroupsParsingFailed, + + InvalidGroup(String), + + InvalidGroupList(String), + /// The given user and group specification was invalid. InvalidUserspec(String), /// The new root directory was not given. MissingNewRoot, + NoGroupSpecified(libc::uid_t), + + /// Failed to find the specified user. + NoSuchUser, + /// Failed to find the specified group. - NoSuchGroup(String), + NoSuchGroup, /// The given directory does not exist. NoSuchDirectory(String), @@ -65,13 +77,18 @@ impl Display for ChrootError { Self::CommandFailed(s, e) | Self::CommandNotFound(s, e) => { write!(f, "failed to run command {}: {}", s.to_string().quote(), e,) } + Self::GroupsParsingFailed => write!(f, "--groups parsing failed"), + Self::InvalidGroup(s) => write!(f, "invalid group: {}", s.quote()), + Self::InvalidGroupList(s) => write!(f, "invalid group list: {}", s.quote()), Self::InvalidUserspec(s) => write!(f, "invalid userspec: {}", s.quote(),), Self::MissingNewRoot => write!( f, "Missing operand: NEWROOT\nTry '{} --help' for more information.", uucore::execution_phrase(), ), - Self::NoSuchGroup(s) => write!(f, "no such group: {}", s.maybe_quote(),), + Self::NoGroupSpecified(uid) => write!(f, "no group specified for unknown uid: {}", uid), + Self::NoSuchUser => write!(f, "invalid user"), + Self::NoSuchGroup => write!(f, "invalid group"), Self::NoSuchDirectory(s) => write!( f, "cannot change root directory to {}: no such directory", diff --git a/src/uu/cksum/Cargo.toml b/src/uu/cksum/Cargo.toml index 18c1b2899f1..c8693190be7 100644 --- a/src/uu/cksum/Cargo.toml +++ b/src/uu/cksum/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_cksum" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "cksum ~ (uutils) display CRC and size of input" diff --git a/src/uu/cksum/src/cksum.rs b/src/uu/cksum/src/cksum.rs index e7d73a3bbf5..b9f74133814 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, @@ -244,16 +244,16 @@ fn had_reset(args: &[OsString]) -> bool { * and "easier" to understand */ fn handle_tag_text_binary_flags(matches: &clap::ArgMatches) -> UResult<(bool, bool)> { - let untagged: bool = matches.get_flag(options::UNTAGGED); - let tag: bool = matches.get_flag(options::TAG); - let tag: bool = tag || !untagged; + let untagged = matches.get_flag(options::UNTAGGED); + let tag = matches.get_flag(options::TAG); + let tag = tag || !untagged; - let binary_flag: bool = matches.get_flag(options::BINARY); + let binary_flag = matches.get_flag(options::BINARY); let args: Vec = std::env::args_os().collect(); let had_reset = had_reset(&args); - let asterisk: bool = prompt_asterisk(tag, binary_flag, had_reset); + let asterisk = prompt_asterisk(tag, binary_flag, had_reset); Ok((tag, asterisk)) } @@ -276,10 +276,6 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } }; - if ["bsd", "crc", "sysv"].contains(&algo_name) && check { - return Err(ChecksumError::AlgorithmNotSupportedWithCheck.into()); - } - let input_length = matches.get_one::(options::LENGTH); let length = match input_length { @@ -293,6 +289,10 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { None => None, }; + if ["bsd", "crc", "sysv"].contains(&algo_name) && check { + return Err(ChecksumError::AlgorithmNotSupportedWithCheck.into()); + } + if check { let text_flag = matches.get_flag(options::TEXT); let binary_flag = matches.get_flag(options::BINARY); @@ -301,8 +301,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let warn = matches.get_flag(options::WARN); let ignore_missing = matches.get_flag(options::IGNORE_MISSING); let quiet = matches.get_flag(options::QUIET); + let tag = matches.get_flag(options::TAG); - if binary_flag || text_flag { + if tag || binary_flag || text_flag { return Err(ChecksumError::BinaryTextConflict.into()); } // Determine the appropriate algorithm option to pass @@ -318,17 +319,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)?; @@ -427,8 +427,7 @@ pub fn uu_app() -> Command { .short('c') .long(options::CHECK) .help("read hashsums from the FILEs and check them") - .action(ArgAction::SetTrue) - .conflicts_with("tag"), + .action(ArgAction::SetTrue), ) .arg( Arg::new(options::BASE64) diff --git a/src/uu/comm/Cargo.toml b/src/uu/comm/Cargo.toml index 26d82dbecc6..ce250c554c3 100644 --- a/src/uu/comm/Cargo.toml +++ b/src/uu/comm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_comm" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "comm ~ (uutils) compare sorted inputs" @@ -18,7 +18,7 @@ path = "src/comm.rs" [dependencies] clap = { workspace = true } -uucore = { workspace = true } +uucore = { workspace = true, features = ["fs"] } [[bin]] name = "comm" diff --git a/src/uu/comm/src/comm.rs b/src/uu/comm/src/comm.rs index cae405865e6..e075830cb85 100644 --- a/src/uu/comm/src/comm.rs +++ b/src/uu/comm/src/comm.rs @@ -3,13 +3,13 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (ToDO) delim mkdelim +// spell-checker:ignore (ToDO) delim mkdelim pairable use std::cmp::Ordering; -use std::fs::File; -use std::io::{self, stdin, BufRead, BufReader, Stdin}; -use std::path::Path; +use std::fs::{metadata, File}; +use std::io::{self, stdin, BufRead, BufReader, Read, Stdin}; use uucore::error::{FromIo, UResult, USimpleError}; +use uucore::fs::paths_refer_to_same_file; use uucore::line_ending::LineEnding; use uucore::{format_usage, help_about, help_usage}; @@ -28,6 +28,30 @@ mod options { pub const FILE_2: &str = "FILE2"; pub const TOTAL: &str = "total"; pub const ZERO_TERMINATED: &str = "zero-terminated"; + pub const CHECK_ORDER: &str = "check-order"; + pub const NO_CHECK_ORDER: &str = "nocheck-order"; +} + +#[derive(Debug, Clone, Copy)] +enum FileNumber { + One, + Two, +} + +impl FileNumber { + fn as_str(&self) -> &'static str { + match self { + FileNumber::One => "1", + FileNumber::Two => "2", + } + } +} + +struct OrderChecker { + last_line: Vec, + file_num: FileNumber, + check_order: bool, + has_error: bool, } enum Input { @@ -61,7 +85,74 @@ impl LineReader { } } -fn comm(a: &mut LineReader, b: &mut LineReader, delim: &str, opts: &ArgMatches) { +impl OrderChecker { + fn new(file_num: FileNumber, check_order: bool) -> Self { + Self { + last_line: Vec::new(), + file_num, + check_order, + has_error: false, + } + } + + fn verify_order(&mut self, current_line: &[u8]) -> bool { + if self.last_line.is_empty() { + self.last_line = current_line.to_vec(); + return true; + } + + let is_ordered = current_line >= &self.last_line; + if !is_ordered && !self.has_error { + eprintln!( + "comm: file {} is not in sorted order", + self.file_num.as_str() + ); + self.has_error = true; + } + + self.last_line = current_line.to_vec(); + is_ordered || !self.check_order + } +} + +// Check if two files are identical by comparing their contents +pub fn are_files_identical(path1: &str, path2: &str) -> io::Result { + // First compare file sizes + let metadata1 = std::fs::metadata(path1)?; + let metadata2 = std::fs::metadata(path2)?; + + if metadata1.len() != metadata2.len() { + return Ok(false); + } + + let file1 = File::open(path1)?; + let file2 = File::open(path2)?; + + let mut reader1 = BufReader::new(file1); + let mut reader2 = BufReader::new(file2); + + let mut buffer1 = [0; 8192]; + let mut buffer2 = [0; 8192]; + + loop { + let bytes1 = reader1.read(&mut buffer1)?; + let bytes2 = reader2.read(&mut buffer2)?; + + if bytes1 != bytes2 { + return Ok(false); + } + + if bytes1 == 0 { + return Ok(true); + } + + if buffer1[..bytes1] != buffer2[..bytes2] { + return Ok(false); + } + } +} + +fn comm(a: &mut LineReader, b: &mut LineReader, delim: &str, opts: &ArgMatches) -> UResult<()> { let width_col_1 = usize::from(!opts.get_flag(options::COLUMN_1)); let width_col_2 = usize::from(!opts.get_flag(options::COLUMN_2)); @@ -77,6 +168,26 @@ fn comm(a: &mut LineReader, b: &mut LineReader, delim: &str, opts: &ArgMatches) let mut total_col_2 = 0; let mut total_col_3 = 0; + let check_order = opts.get_flag(options::CHECK_ORDER); + let no_check_order = opts.get_flag(options::NO_CHECK_ORDER); + + // Determine if we should perform order checking + let should_check_order = !no_check_order + && (check_order + || if let (Some(file1), Some(file2)) = ( + opts.get_one::(options::FILE_1), + opts.get_one::(options::FILE_2), + ) { + !(paths_refer_to_same_file(file1, file2, true) + || are_files_identical(file1, file2).unwrap_or(false)) + } else { + true + }); + + let mut checker1 = OrderChecker::new(FileNumber::One, check_order); + let mut checker2 = OrderChecker::new(FileNumber::Two, check_order); + let mut input_error = false; + while na.is_ok() || nb.is_ok() { let ord = match (na.is_ok(), nb.is_ok()) { (false, true) => Ordering::Greater, @@ -92,6 +203,9 @@ fn comm(a: &mut LineReader, b: &mut LineReader, delim: &str, opts: &ArgMatches) match ord { Ordering::Less => { + if should_check_order && !checker1.verify_order(ra) { + break; + } if !opts.get_flag(options::COLUMN_1) { print!("{}", String::from_utf8_lossy(ra)); } @@ -100,6 +214,9 @@ fn comm(a: &mut LineReader, b: &mut LineReader, delim: &str, opts: &ArgMatches) total_col_1 += 1; } Ordering::Greater => { + if should_check_order && !checker2.verify_order(rb) { + break; + } if !opts.get_flag(options::COLUMN_2) { print!("{delim_col_2}{}", String::from_utf8_lossy(rb)); } @@ -108,6 +225,10 @@ fn comm(a: &mut LineReader, b: &mut LineReader, delim: &str, opts: &ArgMatches) total_col_2 += 1; } Ordering::Equal => { + if should_check_order && (!checker1.verify_order(ra) || !checker2.verify_order(rb)) + { + break; + } if !opts.get_flag(options::COLUMN_3) { print!("{delim_col_3}{}", String::from_utf8_lossy(ra)); } @@ -118,19 +239,37 @@ fn comm(a: &mut LineReader, b: &mut LineReader, delim: &str, opts: &ArgMatches) total_col_3 += 1; } } + + // Track if we've seen any order errors + if (checker1.has_error || checker2.has_error) && !input_error && !check_order { + input_error = true; + } } if opts.get_flag(options::TOTAL) { let line_ending = LineEnding::from_zero_flag(opts.get_flag(options::ZERO_TERMINATED)); print!("{total_col_1}{delim}{total_col_2}{delim}{total_col_3}{delim}total{line_ending}"); } + + if should_check_order && (checker1.has_error || checker2.has_error) { + // Print the input error message once at the end + if input_error { + eprintln!("comm: input is not in sorted order"); + } + Err(USimpleError::new(1, "")) + } else { + Ok(()) + } } 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, @@ -168,8 +307,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { "" => "\0", delim => delim, }; - comm(&mut f1, &mut f2, delim, &matches); - Ok(()) + + comm(&mut f1, &mut f2, delim, &matches) } pub fn uu_app() -> Command { @@ -231,4 +370,17 @@ pub fn uu_app() -> Command { .help("output a summary") .action(ArgAction::SetTrue), ) + .arg( + Arg::new(options::CHECK_ORDER) + .long(options::CHECK_ORDER) + .help("check that the input is correctly sorted, even if all input lines are pairable") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new(options::NO_CHECK_ORDER) + .long(options::NO_CHECK_ORDER) + .help("do not check that the input is correctly sorted") + .action(ArgAction::SetTrue) + .conflicts_with(options::CHECK_ORDER), + ) } diff --git a/src/uu/cp/Cargo.toml b/src/uu/cp/Cargo.toml index 6801e6a0960..ebcd8ff877e 100644 --- a/src/uu/cp/Cargo.toml +++ b/src/uu/cp/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_cp" -version = "0.0.28" +version = "0.0.29" authors = [ "Jordy Dickinson ", "Joshua S. Miller ", @@ -28,8 +28,10 @@ quick-error = { workspace = true } selinux = { workspace = true, optional = true } uucore = { workspace = true, features = [ "backup-control", + "buf-copy", "entries", "fs", + "fsxattr", "perms", "mode", "update-control", diff --git a/src/uu/cp/src/copydir.rs b/src/uu/cp/src/copydir.rs index 3bfc5524913..bd81a39f5da 100644 --- a/src/uu/cp/src/copydir.rs +++ b/src/uu/cp/src/copydir.rs @@ -79,6 +79,12 @@ fn get_local_to_root_parent( } } +/// Given an iterator, return all its items except the last. +fn skip_last(mut iter: impl Iterator) -> impl Iterator { + let last = iter.next(); + iter.scan(last, |state, item| std::mem::replace(state, Some(item))) +} + /// Paths that are invariant throughout the traversal when copying a directory. struct Context<'a> { /// The current working directory at the time of starting the traversal. @@ -162,17 +168,18 @@ struct Entry { } impl Entry { - fn new( + fn new>( context: &Context, - direntry: &DirEntry, + source: A, no_target_dir: bool, ) -> Result { - let source_relative = direntry.path().to_path_buf(); + let source = source.as_ref(); + let source_relative = source.to_path_buf(); let source_absolute = context.current_dir.join(&source_relative); let mut descendant = get_local_to_root_parent(&source_absolute, context.root_parent.as_deref())?; if no_target_dir { - let source_is_dir = direntry.path().is_dir(); + let source_is_dir = source.is_dir(); if path_ends_with_terminator(context.target) && source_is_dir { if let Err(e) = std::fs::create_dir_all(context.target) { eprintln!("Failed to create directory: {e}"); @@ -213,6 +220,7 @@ where // `path.ends_with(".")` does not seem to work path.as_ref().display().to_string().ends_with("/.") } + #[allow(clippy::too_many_arguments)] /// Copy a single entry during a directory traversal. fn copy_direntry( @@ -246,7 +254,7 @@ fn copy_direntry( if target_is_file { return Err("cannot overwrite non-directory with directory".into()); } else { - build_dir(options, &local_to_target, false)?; + build_dir(&local_to_target, false, options, Some(&source_absolute))?; if options.verbose { println!("{}", context_for(&source_relative, &local_to_target)); } @@ -371,7 +379,7 @@ pub(crate) fn copy_directory( let tmp = if options.parents { if let Some(parent) = root.parent() { let new_target = target.join(parent); - build_dir(options, &new_target, true)?; + build_dir(&new_target, true, options, None)?; if options.verbose { // For example, if copying file `a/b/c` and its parents // to directory `d/`, then print @@ -403,6 +411,9 @@ pub(crate) fn copy_directory( Err(e) => return Err(format!("failed to get current directory {e}").into()), }; + // The directory we were in during the previous iteration + let mut last_iter: Option = None; + // Traverse the contents of the directory, copying each one. for direntry_result in WalkDir::new(root) .same_file_system(options.one_file_system) @@ -410,7 +421,8 @@ pub(crate) fn copy_directory( { match direntry_result { Ok(direntry) => { - let entry = Entry::new(&context, &direntry, options.no_target_dir)?; + let entry = Entry::new(&context, direntry.path(), options.no_target_dir)?; + copy_direntry( progress_bar, entry, @@ -420,23 +432,93 @@ pub(crate) fn copy_directory( copied_destinations, copied_files, )?; + + // We omit certain permissions when creating directories + // to prevent other users from accessing them before they're done. + // We thus need to fix the permissions of each directory we copy + // once it's contents are ready. + // This "fixup" is implemented here in a memory-efficient manner. + // + // We detect iterations where we "walk up" the directory tree, + // and fix permissions on all the directories we exited. + // (Note that there can be more than one! We might step out of + // `./a/b/c` into `./a/`, in which case we'll need to fix the + // permissions of both `./a/b/c` and `./a/b`, in that order.) + if direntry.file_type().is_dir() { + // If true, last_iter is not a parent of this iter. + // The means we just exited a directory. + let went_up = if let Some(last_iter) = &last_iter { + last_iter.path().strip_prefix(direntry.path()).is_ok() + } else { + false + }; + + if went_up { + // Compute the "difference" between `last_iter` and `direntry`. + // For example, if... + // - last_iter = `a/b/c/d` + // - direntry = `a/b` + // then diff = `c/d` + // + // All the unwraps() here are unreachable. + let last_iter = last_iter.as_ref().unwrap(); + let diff = last_iter.path().strip_prefix(direntry.path()).unwrap(); + + // Fix permissions for every entry in `diff`, inside-out. + // We skip the last directory (which will be `.`) because + // its permissions will be fixed when we walk _out_ of it. + // (at this point, we might not be done copying `.`!) + for p in skip_last(diff.ancestors()) { + let src = direntry.path().join(p); + let entry = Entry::new(&context, &src, options.no_target_dir)?; + + copy_attributes( + &entry.source_absolute, + &entry.local_to_target, + &options.attributes, + )?; + } + } + + last_iter = Some(direntry); + } } + // Print an error message, but continue traversing the directory. Err(e) => show_error!("{}", e), } } - // Copy the attributes from the root directory to the target directory. + // Handle final directory permission fixes. + // This is almost the same as the permission-fixing code above, + // with minor differences (commented) + if let Some(last_iter) = last_iter { + let diff = last_iter.path().strip_prefix(root).unwrap(); + + // Do _not_ skip `.` this time, since we know we're done. + // This is where we fix the permissions of the top-level + // directory we just copied. + for p in diff.ancestors() { + let src = root.join(p); + let entry = Entry::new(&context, &src, options.no_target_dir)?; + + copy_attributes( + &entry.source_absolute, + &entry.local_to_target, + &options.attributes, + )?; + } + } + + // Also fix permissions for parent directories, + // if we were asked to create them. if options.parents { let dest = target.join(root.file_name().unwrap()); - copy_attributes(root, dest.as_path(), &options.attributes)?; for (x, y) in aligned_ancestors(root, dest.as_path()) { if let Ok(src) = canonicalize(x, MissingHandling::Normal, ResolveMode::Physical) { copy_attributes(&src, y, &options.attributes)?; } } - } else { - copy_attributes(root, target, &options.attributes)?; } Ok(()) @@ -469,20 +551,32 @@ pub fn path_has_prefix(p1: &Path, p2: &Path) -> io::Result { /// Builds a directory at the specified path with the given options. /// /// # Notes -/// - It excludes certain permissions if ownership or special mode bits could -/// potentially change. +/// - If `copy_attributes_from` is `Some`, the new directory's attributes will be +/// copied from the provided file. Otherwise, the new directory will have the default +/// attributes for the current user. +/// - This method excludes certain permissions if ownership or special mode bits could +/// potentially change. (See `test_dir_perm_race_with_preserve_mode_and_ownership``) /// - The `recursive` flag determines whether parent directories should be created /// if they do not already exist. // we need to allow unused_variable since `options` might be unused in non unix systems #[allow(unused_variables)] -fn build_dir(options: &Options, path: &PathBuf, recursive: bool) -> CopyResult<()> { +fn build_dir( + path: &PathBuf, + recursive: bool, + options: &Options, + copy_attributes_from: Option<&Path>, +) -> CopyResult<()> { let mut builder = fs::DirBuilder::new(); builder.recursive(recursive); + // To prevent unauthorized access before the folder is ready, // exclude certain permissions if ownership or special mode bits // could potentially change. #[cfg(unix)] { + use crate::Preserve; + use std::os::unix::fs::PermissionsExt; + // we need to allow trivial casts here because some systems like linux have u32 constants in // in libc while others don't. #[allow(clippy::unnecessary_cast)] @@ -494,10 +588,22 @@ fn build_dir(options: &Options, path: &PathBuf, recursive: bool) -> CopyResult<( } else { 0 } as u32; - excluded_perms |= uucore::mode::get_umask(); + + let umask = if copy_attributes_from.is_some() + && matches!(options.attributes.mode, Preserve::Yes { .. }) + { + !fs::symlink_metadata(copy_attributes_from.unwrap())? + .permissions() + .mode() + } else { + uucore::mode::get_umask() + }; + + excluded_perms |= umask; let mode = !excluded_perms & 0o777; //use only the last three octet bits std::os::unix::fs::DirBuilderExt::mode(&mut builder, mode); } + builder.create(path)?; Ok(()) } diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index 32168b09009..626b65ad63e 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -10,13 +10,15 @@ use std::collections::{HashMap, HashSet}; #[cfg(not(windows))] use std::ffi::CString; use std::ffi::OsString; -use std::fs::{self, File, Metadata, OpenOptions, Permissions}; +use std::fs::{self, Metadata, OpenOptions, Permissions}; use std::io; #[cfg(unix)] 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; @@ -172,7 +174,7 @@ pub enum CopyMode { /// For full compatibility with GNU, these options should also combine. We /// currently only do a best effort imitation of that behavior, because it is /// difficult to achieve in clap, especially with `--no-preserve`. -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone, Copy)] pub struct Attributes { #[cfg(unix)] pub ownership: Preserve, @@ -930,6 +932,16 @@ impl Options { }; let update_mode = update_control::determine_update_mode(matches); + if backup_mode != BackupMode::NoBackup + && matches + .get_one::(update_control::arguments::OPT_UPDATE) + .is_some_and(|v| v == "none" || v == "none-fail") + { + return Err(Error::InvalidArgument( + "--backup is mutually exclusive with -n or --update=none-fail".to_string(), + )); + } + let backup_suffix = backup_control::determine_backup_suffix(matches); let overwrite = OverwriteMode::from_matches(matches); @@ -1516,6 +1528,42 @@ fn handle_preserve CopyResult<()>>(p: &Preserve, f: F) -> CopyResult< Ok(()) } +/// Copies extended attributes (xattrs) from `source` to `dest`, ensuring that `dest` is temporarily +/// user-writable if needed and restoring its original permissions afterward. This avoids “Operation +/// not permitted” errors on read-only files. Returns an error if permission or metadata operations fail, +/// or if xattr copying fails. +#[cfg(all(unix, not(target_os = "android")))] +fn copy_extended_attrs(source: &Path, dest: &Path) -> CopyResult<()> { + let metadata = fs::symlink_metadata(dest)?; + + // Check if the destination file is currently read-only for the user. + let mut perms = metadata.permissions(); + let was_readonly = perms.readonly(); + + // Temporarily grant user write if it was read-only. + if was_readonly { + #[allow(clippy::permissions_set_readonly_false)] + perms.set_readonly(false); + fs::set_permissions(dest, perms)?; + } + + // Perform the xattr copy and capture any potential error, + // so we can restore permissions before returning. + let copy_xattrs_result = copy_xattrs(source, dest); + + // Restore read-only if we changed it. + if was_readonly { + let mut revert_perms = fs::symlink_metadata(dest)?.permissions(); + revert_perms.set_readonly(true); + fs::set_permissions(dest, revert_perms)?; + } + + // If copying xattrs failed, propagate that error now. + copy_xattrs_result?; + + Ok(()) +} + /// Copy the specified attributes from one path to another. pub(crate) fn copy_attributes( source: &Path, @@ -1605,12 +1653,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_extended_attrs(source, dest)?; } #[cfg(not(all(unix, not(target_os = "android"))))] { @@ -1920,6 +1963,7 @@ fn print_paths(parents: bool, source: &Path, dest: &Path) { /// /// * `Ok(())` - The file was copied successfully. /// * `Err(CopyError)` - An error occurred while copying the file. +#[allow(clippy::too_many_arguments)] fn handle_copy_mode( source: &Path, dest: &Path, @@ -1928,15 +1972,10 @@ fn handle_copy_mode( source_metadata: &Metadata, symlinked_files: &mut HashSet, source_in_command_line: bool, + source_is_fifo: bool, + #[cfg(unix)] source_is_stream: bool, ) -> CopyResult<()> { - let source_file_type = source_metadata.file_type(); - - let source_is_symlink = source_file_type.is_symlink(); - - #[cfg(unix)] - let source_is_fifo = source_file_type.is_fifo(); - #[cfg(not(unix))] - let source_is_fifo = false; + let source_is_symlink = source_metadata.is_symlink(); match options.copy_mode { CopyMode::Link => { @@ -1973,6 +2012,8 @@ fn handle_copy_mode( source_is_symlink, source_is_fifo, symlinked_files, + #[cfg(unix)] + source_is_stream, )?; } CopyMode::SymLink => { @@ -1993,6 +2034,8 @@ fn handle_copy_mode( source_is_symlink, source_is_fifo, symlinked_files, + #[cfg(unix)] + source_is_stream, )?; } update_control::UpdateMode::ReplaceNone => { @@ -2023,6 +2066,8 @@ fn handle_copy_mode( source_is_symlink, source_is_fifo, symlinked_files, + #[cfg(unix)] + source_is_stream, )?; } } @@ -2036,6 +2081,8 @@ fn handle_copy_mode( source_is_symlink, source_is_fifo, symlinked_files, + #[cfg(unix)] + source_is_stream, )?; } } @@ -2262,6 +2309,18 @@ fn copy_file( let dest_permissions = calculate_dest_permissions(dest, &source_metadata, options, context)?; + #[cfg(unix)] + let source_is_fifo = source_metadata.file_type().is_fifo(); + #[cfg(not(unix))] + let source_is_fifo = false; + + #[cfg(unix)] + let source_is_stream = source_is_fifo + || source_metadata.file_type().is_char_device() + || source_metadata.file_type().is_block_device(); + #[cfg(not(unix))] + let source_is_stream = false; + handle_copy_mode( source, dest, @@ -2270,6 +2329,9 @@ fn copy_file( &source_metadata, symlinked_files, source_in_command_line, + source_is_fifo, + #[cfg(unix)] + source_is_stream, )?; // TODO: implement something similar to gnu's lchown @@ -2285,8 +2347,16 @@ fn copy_file( if options.dereference(source_in_command_line) { if let Ok(src) = canonicalize(source, MissingHandling::Normal, ResolveMode::Physical) { - copy_attributes(&src, dest, &options.attributes)?; + if src.exists() { + copy_attributes(&src, dest, &options.attributes)?; + } } + } else if source_is_stream && source.exists() { + // Some stream files may not exist after we have copied it, + // like anonymous pipes. Thus, we can't really copy its + // attributes. However, this is already handled in the stream + // copy function (see `copy_stream` under platform/linux.rs). + copy_attributes(source, dest, &options.attributes)?; } else { copy_attributes(source, dest, &options.attributes)?; } @@ -2350,6 +2420,7 @@ fn handle_no_preserve_mode(options: &Options, org_mode: u32) -> u32 { /// Copy the file from `source` to `dest` either using the normal `fs::copy` or a /// copy-on-write scheme if --reflink is specified and the filesystem supports it. +#[allow(clippy::too_many_arguments)] fn copy_helper( source: &Path, dest: &Path, @@ -2358,6 +2429,7 @@ fn copy_helper( source_is_symlink: bool, source_is_fifo: bool, symlinked_files: &mut HashSet, + #[cfg(unix)] source_is_stream: bool, ) -> CopyResult<()> { if options.parents { let parent = dest.parent().unwrap_or(dest); @@ -2368,12 +2440,7 @@ fn copy_helper( return Err(Error::NotADirectory(dest.to_path_buf())); } - if source.as_os_str() == "/dev/null" { - /* workaround a limitation of fs::copy - * https://github.com/rust-lang/rust/issues/79390 - */ - File::create(dest).context(dest.display().to_string())?; - } else if source_is_fifo && options.recursive && !options.copy_contents { + if source_is_fifo && options.recursive && !options.copy_contents { #[cfg(unix)] copy_fifo(dest, options.overwrite, options.debug)?; } else if source_is_symlink { @@ -2385,8 +2452,10 @@ fn copy_helper( options.reflink_mode, options.sparse_mode, context, - #[cfg(any(target_os = "linux", target_os = "android", target_os = "macos"))] + #[cfg(unix)] source_is_fifo, + #[cfg(unix)] + source_is_stream, )?; if !options.attributes_only && options.debug { diff --git a/src/uu/cp/src/platform/linux.rs b/src/uu/cp/src/platform/linux.rs index 949bd5e03c7..0ca39a75ef2 100644 --- a/src/uu/cp/src/platform/linux.rs +++ b/src/uu/cp/src/platform/linux.rs @@ -12,6 +12,7 @@ use std::os::unix::fs::MetadataExt; use std::os::unix::fs::{FileTypeExt, OpenOptionsExt}; use std::os::unix::io::AsRawFd; use std::path::Path; +use uucore::buf_copy; use quick_error::ResultExt; @@ -220,8 +221,9 @@ fn check_dest_is_fifo(dest: &Path) -> bool { } } -/// Copy the contents of the given source FIFO to the given file. -fn copy_fifo_contents

(source: P, dest: P) -> std::io::Result +/// Copy the contents of a stream from `source` to `dest`. The `if_fifo` argument is used to +/// determine if we need to modify the file's attributes before and after copying. +fn copy_stream

(source: P, dest: P, is_fifo: bool) -> std::io::Result where P: AsRef, { @@ -250,8 +252,14 @@ where .write(true) .mode(mode) .open(&dest)?; - let num_bytes_copied = std::io::copy(&mut src_file, &mut dst_file)?; - dst_file.set_permissions(src_file.metadata()?.permissions())?; + + let num_bytes_copied = buf_copy::copy_stream(&mut src_file, &mut dst_file) + .map_err(|_| std::io::Error::from(std::io::ErrorKind::Other))?; + + if is_fifo { + dst_file.set_permissions(src_file.metadata()?.permissions())?; + } + Ok(num_bytes_copied) } @@ -268,6 +276,7 @@ pub(crate) fn copy_on_write( sparse_mode: SparseMode, context: &str, source_is_fifo: bool, + source_is_stream: bool, ) -> CopyResult { let mut copy_debug = CopyDebug { offload: OffloadReflinkDebug::Unknown, @@ -279,10 +288,9 @@ pub(crate) fn copy_on_write( copy_debug.sparse_detection = SparseDebug::Zeros; // Default SparseDebug val for SparseMode::Always copy_debug.reflink = OffloadReflinkDebug::No; - if source_is_fifo { + if source_is_stream { copy_debug.offload = OffloadReflinkDebug::Avoided; - - copy_fifo_contents(source, dest).map(|_| ()) + copy_stream(source, dest, source_is_fifo).map(|_| ()) } else { let mut copy_method = CopyMethod::Default; let result = handle_reflink_never_sparse_always(source, dest); @@ -300,10 +308,9 @@ pub(crate) fn copy_on_write( (ReflinkMode::Never, SparseMode::Never) => { copy_debug.reflink = OffloadReflinkDebug::No; - if source_is_fifo { + if source_is_stream { copy_debug.offload = OffloadReflinkDebug::Avoided; - - copy_fifo_contents(source, dest).map(|_| ()) + copy_stream(source, dest, source_is_fifo).map(|_| ()) } else { let result = handle_reflink_never_sparse_never(source); if let Ok(debug) = result { @@ -315,9 +322,9 @@ pub(crate) fn copy_on_write( (ReflinkMode::Never, SparseMode::Auto) => { copy_debug.reflink = OffloadReflinkDebug::No; - if source_is_fifo { + if source_is_stream { copy_debug.offload = OffloadReflinkDebug::Avoided; - copy_fifo_contents(source, dest).map(|_| ()) + copy_stream(source, dest, source_is_fifo).map(|_| ()) } else { let mut copy_method = CopyMethod::Default; let result = handle_reflink_never_sparse_auto(source, dest); @@ -335,10 +342,9 @@ pub(crate) fn copy_on_write( (ReflinkMode::Auto, SparseMode::Always) => { copy_debug.sparse_detection = SparseDebug::Zeros; // Default SparseDebug val for // SparseMode::Always - if source_is_fifo { + if source_is_stream { copy_debug.offload = OffloadReflinkDebug::Avoided; - - copy_fifo_contents(source, dest).map(|_| ()) + copy_stream(source, dest, source_is_fifo).map(|_| ()) } else { let mut copy_method = CopyMethod::Default; let result = handle_reflink_auto_sparse_always(source, dest); @@ -356,9 +362,9 @@ pub(crate) fn copy_on_write( (ReflinkMode::Auto, SparseMode::Never) => { copy_debug.reflink = OffloadReflinkDebug::No; - if source_is_fifo { + if source_is_stream { copy_debug.offload = OffloadReflinkDebug::Avoided; - copy_fifo_contents(source, dest).map(|_| ()) + copy_stream(source, dest, source_is_fifo).map(|_| ()) } else { let result = handle_reflink_auto_sparse_never(source); if let Ok(debug) = result { @@ -369,9 +375,9 @@ pub(crate) fn copy_on_write( } } (ReflinkMode::Auto, SparseMode::Auto) => { - if source_is_fifo { + if source_is_stream { copy_debug.offload = OffloadReflinkDebug::Unsupported; - copy_fifo_contents(source, dest).map(|_| ()) + copy_stream(source, dest, source_is_fifo).map(|_| ()) } else { let mut copy_method = CopyMethod::Default; let result = handle_reflink_auto_sparse_auto(source, dest); diff --git a/src/uu/cp/src/platform/macos.rs b/src/uu/cp/src/platform/macos.rs index 77bdbbbdb83..988dc6b2536 100644 --- a/src/uu/cp/src/platform/macos.rs +++ b/src/uu/cp/src/platform/macos.rs @@ -4,12 +4,14 @@ // file that was distributed with this source code. // spell-checker:ignore reflink use std::ffi::CString; -use std::fs::{self, File}; -use std::io; +use std::fs::{self, File, OpenOptions}; use std::os::unix::ffi::OsStrExt; +use std::os::unix::fs::OpenOptionsExt; use std::path::Path; use quick_error::ResultExt; +use uucore::buf_copy; +use uucore::mode::get_umask; use crate::{CopyDebug, CopyResult, OffloadReflinkDebug, ReflinkMode, SparseDebug, SparseMode}; @@ -24,6 +26,7 @@ pub(crate) fn copy_on_write( sparse_mode: SparseMode, context: &str, source_is_fifo: bool, + source_is_stream: bool, ) -> CopyResult { if sparse_mode != SparseMode::Auto { return Err("--sparse is only supported on linux".to_string().into()); @@ -64,7 +67,7 @@ pub(crate) fn copy_on_write( // clonefile(2) fails if the destination exists. Remove it and try again. Do not // bother to check if removal worked because we're going to try to clone again. // first lets make sure the dest file is not read only - if fs::metadata(dest).map_or(false, |md| !md.permissions().readonly()) { + if fs::metadata(dest).is_ok_and(|md| !md.permissions().readonly()) { // remove and copy again // TODO: rewrite this to better match linux behavior // linux first opens the source file and destination file then uses the file @@ -85,10 +88,23 @@ pub(crate) fn copy_on_write( } _ => { copy_debug.reflink = OffloadReflinkDebug::Yes; - if source_is_fifo { + if source_is_stream { let mut src_file = File::open(source)?; - let mut dst_file = File::create(dest)?; - io::copy(&mut src_file, &mut dst_file).context(context)? + let mode = 0o622 & !get_umask(); + let mut dst_file = OpenOptions::new() + .create(true) + .write(true) + .mode(mode) + .open(dest)?; + + let context = buf_copy::copy_stream(&mut src_file, &mut dst_file) + .map_err(|_| std::io::Error::from(std::io::ErrorKind::Other)) + .context(context)?; + + if source_is_fifo { + dst_file.set_permissions(src_file.metadata()?.permissions())?; + } + context } else { fs::copy(source, dest).context(context)? } diff --git a/src/uu/cp/src/platform/mod.rs b/src/uu/cp/src/platform/mod.rs index c7942706868..2071e928f41 100644 --- a/src/uu/cp/src/platform/mod.rs +++ b/src/uu/cp/src/platform/mod.rs @@ -2,6 +2,18 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. + +#[cfg(all( + unix, + not(any(target_os = "macos", target_os = "linux", target_os = "android")) +))] +mod other_unix; +#[cfg(all( + unix, + not(any(target_os = "macos", target_os = "linux", target_os = "android")) +))] +pub(crate) use self::other_unix::copy_on_write; + #[cfg(target_os = "macos")] mod macos; #[cfg(target_os = "macos")] @@ -12,7 +24,13 @@ mod linux; #[cfg(any(target_os = "linux", target_os = "android"))] pub(crate) use self::linux::copy_on_write; -#[cfg(not(any(target_os = "linux", target_os = "android", target_os = "macos")))] +#[cfg(not(any( + unix, + any(target_os = "macos", target_os = "linux", target_os = "android") +)))] mod other; -#[cfg(not(any(target_os = "linux", target_os = "android", target_os = "macos")))] +#[cfg(not(any( + unix, + any(target_os = "macos", target_os = "linux", target_os = "android") +)))] pub(crate) use self::other::copy_on_write; diff --git a/src/uu/cp/src/platform/other_unix.rs b/src/uu/cp/src/platform/other_unix.rs new file mode 100644 index 00000000000..aa8fed3fab1 --- /dev/null +++ b/src/uu/cp/src/platform/other_unix.rs @@ -0,0 +1,62 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. +// spell-checker:ignore reflink +use std::fs::{self, File, OpenOptions}; +use std::os::unix::fs::OpenOptionsExt; +use std::path::Path; + +use quick_error::ResultExt; +use uucore::buf_copy; +use uucore::mode::get_umask; + +use crate::{CopyDebug, CopyResult, OffloadReflinkDebug, ReflinkMode, SparseDebug, SparseMode}; + +/// Copies `source` to `dest` for systems without copy-on-write +pub(crate) fn copy_on_write( + source: &Path, + dest: &Path, + reflink_mode: ReflinkMode, + sparse_mode: SparseMode, + context: &str, + source_is_fifo: bool, + source_is_stream: bool, +) -> CopyResult { + if reflink_mode != ReflinkMode::Never { + return Err("--reflink is only supported on linux and macOS" + .to_string() + .into()); + } + if sparse_mode != SparseMode::Auto { + return Err("--sparse is only supported on linux".to_string().into()); + } + let copy_debug = CopyDebug { + offload: OffloadReflinkDebug::Unsupported, + reflink: OffloadReflinkDebug::Unsupported, + sparse_detection: SparseDebug::Unsupported, + }; + + if source_is_stream { + let mut src_file = File::open(source)?; + let mode = 0o622 & !get_umask(); + let mut dst_file = OpenOptions::new() + .create(true) + .write(true) + .mode(mode) + .open(dest)?; + + buf_copy::copy_stream(&mut src_file, &mut dst_file) + .map_err(|_| std::io::Error::from(std::io::ErrorKind::Other)) + .context(context)?; + + if source_is_fifo { + dst_file.set_permissions(src_file.metadata()?.permissions())?; + } + return Ok(copy_debug); + } + + fs::copy(source, dest).context(context)?; + + Ok(copy_debug) +} diff --git a/src/uu/csplit/Cargo.toml b/src/uu/csplit/Cargo.toml index b2771d8fe52..ec726e9d2b2 100644 --- a/src/uu/csplit/Cargo.toml +++ b/src/uu/csplit/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_csplit" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "csplit ~ (uutils) Output pieces of FILE separated by PATTERN(s) to files 'xx00', 'xx01', ..., and output byte counts of each piece to standard output" diff --git a/src/uu/csplit/src/csplit.rs b/src/uu/csplit/src/csplit.rs index 9e132b704bf..501f97582ec 100644 --- a/src/uu/csplit/src/csplit.rs +++ b/src/uu/csplit/src/csplit.rs @@ -16,7 +16,7 @@ use clap::{crate_version, Arg, ArgAction, ArgMatches, Command}; use regex::Regex; use uucore::display::Quotable; use uucore::error::{FromIo, UResult}; -use uucore::{crash_if_err, format_usage, help_about, help_section, help_usage}; +use uucore::{format_usage, help_about, help_section, help_usage}; mod csplit_error; mod patterns; @@ -51,26 +51,23 @@ pub struct CsplitOptions { } impl CsplitOptions { - fn new(matches: &ArgMatches) -> Self { + fn new(matches: &ArgMatches) -> Result { let keep_files = matches.get_flag(options::KEEP_FILES); let quiet = matches.get_flag(options::QUIET); let elide_empty_files = matches.get_flag(options::ELIDE_EMPTY_FILES); let suppress_matched = matches.get_flag(options::SUPPRESS_MATCHED); - Self { - split_name: crash_if_err!( - 1, - SplitName::new( - matches.get_one::(options::PREFIX).cloned(), - matches.get_one::(options::SUFFIX_FORMAT).cloned(), - matches.get_one::(options::DIGITS).cloned() - ) - ), + Ok(Self { + split_name: SplitName::new( + matches.get_one::(options::PREFIX).cloned(), + matches.get_one::(options::SUFFIX_FORMAT).cloned(), + matches.get_one::(options::DIGITS).cloned(), + )?, keep_files, quiet, elide_empty_files, suppress_matched, - } + }) } } @@ -90,7 +87,11 @@ pub fn csplit(options: &CsplitOptions, patterns: &[String], input: T) -> Resu where T: BufRead, { - let mut input_iter = InputSplitter::new(input.lines().enumerate()); + let enumerated_input_lines = input + .lines() + .map(|line| line.map_err_context(|| "read error".to_string())) + .enumerate(); + let mut input_iter = InputSplitter::new(enumerated_input_lines); let mut split_writer = SplitWriter::new(options); let patterns: Vec = patterns::get_patterns(patterns)?; let ret = do_csplit(&mut split_writer, patterns, &mut input_iter); @@ -120,7 +121,7 @@ fn do_csplit( input_iter: &mut InputSplitter, ) -> Result<(), CsplitError> where - I: Iterator)>, + I: Iterator)>, { // split the file based on patterns for pattern in patterns { @@ -197,7 +198,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 +207,7 @@ impl<'a> Drop for SplitWriter<'a> { } } -impl<'a> SplitWriter<'a> { +impl SplitWriter<'_> { fn new(options: &CsplitOptions) -> SplitWriter { SplitWriter { options, @@ -308,7 +309,7 @@ impl<'a> SplitWriter<'a> { input_iter: &mut InputSplitter, ) -> Result<(), CsplitError> where - I: Iterator)>, + I: Iterator)>, { input_iter.rewind_buffer(); input_iter.set_size_of_buffer(1); @@ -361,7 +362,7 @@ impl<'a> SplitWriter<'a> { input_iter: &mut InputSplitter, ) -> Result<(), CsplitError> where - I: Iterator)>, + I: Iterator)>, { if offset >= 0 { // The offset is zero or positive, no need for a buffer on the lines read. @@ -375,6 +376,7 @@ impl<'a> SplitWriter<'a> { while let Some((ln, line)) = input_iter.next() { let l = line?; if regex.is_match(&l) { + let mut next_line_suppress_matched = false; match (self.options.suppress_matched, offset) { // no offset, add the line to the next split (false, 0) => { @@ -385,6 +387,11 @@ impl<'a> SplitWriter<'a> { } // a positive offset, some more lines need to be added to the current split (false, _) => self.writeln(&l)?, + // suppress matched option true, but there is a positive offset, so the line is printed + (true, 1..) => { + next_line_suppress_matched = true; + self.writeln(&l)?; + } _ => (), }; offset -= 1; @@ -405,6 +412,11 @@ impl<'a> SplitWriter<'a> { offset -= 1; } self.finish_split(); + + // if we have to suppress one line after we take the next and do nothing + if next_line_suppress_matched { + input_iter.next(); + } return Ok(()); } self.writeln(&l)?; @@ -423,7 +435,12 @@ impl<'a> SplitWriter<'a> { for line in input_iter.shrink_buffer_to_size() { self.writeln(&line)?; } - if !self.options.suppress_matched { + if self.options.suppress_matched { + // since offset_usize is for sure greater than 0 + // the first element of the buffer should be removed and this + // line inserted to be coherent with GNU implementation + input_iter.add_line_to_buffer(ln, l); + } else { // add 1 to the buffer size to make place for the matched line input_iter.set_size_of_buffer(offset_usize + 1); assert!( @@ -431,6 +448,7 @@ impl<'a> SplitWriter<'a> { "should be big enough to hold every lines" ); } + self.finish_split(); if input_iter.buffer_len() < offset_usize { return Err(CsplitError::LineOutOfRange(pattern_as_str.to_string())); @@ -456,7 +474,7 @@ impl<'a> SplitWriter<'a> { /// This is used to pass matching lines to the next split and to support patterns with a negative offset. struct InputSplitter where - I: Iterator)>, + I: Iterator)>, { iter: I, buffer: Vec<::Item>, @@ -469,7 +487,7 @@ where impl InputSplitter where - I: Iterator)>, + I: Iterator)>, { fn new(iter: I) -> Self { Self { @@ -533,7 +551,7 @@ where impl Iterator for InputSplitter where - I: Iterator)>, + I: Iterator)>, { type Item = ::Item; @@ -561,19 +579,13 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { .unwrap() .map(|s| s.to_string()) .collect(); - let options = CsplitOptions::new(&matches); + let options = CsplitOptions::new(&matches)?; if file_name == "-" { let stdin = io::stdin(); Ok(csplit(&options, &patterns, stdin.lock())?) } else { let file = File::open(file_name) - .map_err_context(|| format!("cannot access {}", file_name.quote()))?; - let file_metadata = file - .metadata() - .map_err_context(|| format!("cannot access {}", file_name.quote()))?; - if !file_metadata.is_file() { - return Err(CsplitError::NotRegularFile(file_name.to_string()).into()); - } + .map_err_context(|| format!("cannot open {} for reading", file_name.quote()))?; Ok(csplit(&options, &patterns, BufReader::new(file))?) } } @@ -621,8 +633,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/csplit_error.rs b/src/uu/csplit/src/csplit_error.rs index 4a83b637b07..ac1c8d01c48 100644 --- a/src/uu/csplit/src/csplit_error.rs +++ b/src/uu/csplit/src/csplit_error.rs @@ -35,6 +35,8 @@ pub enum CsplitError { SuffixFormatTooManyPercents, #[error("{} is not a regular file", ._0.quote())] NotRegularFile(String), + #[error("{}", _0)] + UError(Box), } impl From for CsplitError { @@ -43,8 +45,17 @@ impl From for CsplitError { } } +impl From> for CsplitError { + fn from(error: Box) -> Self { + Self::UError(error) + } +} + impl UError for CsplitError { fn code(&self) -> i32 { - 1 + match self { + Self::UError(e) => e.code(), + _ => 1, + } } } diff --git a/src/uu/csplit/src/patterns.rs b/src/uu/csplit/src/patterns.rs index bd6c4fbfaef..edd632d08fc 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/Cargo.toml b/src/uu/cut/Cargo.toml index 33046e77321..4a41b4fac4f 100644 --- a/src/uu/cut/Cargo.toml +++ b/src/uu/cut/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_cut" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "cut ~ (uutils) display byte/field columns of input lines" diff --git a/src/uu/cut/src/cut.rs b/src/uu/cut/src/cut.rs index cd6eb22d30a..5e128425b63 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 953e083b139..bb0c44d5bb4 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 21424790eea..41c12cf6e2f 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/Cargo.toml b/src/uu/date/Cargo.toml index a99f284713a..87e8d383a75 100644 --- a/src/uu/date/Cargo.toml +++ b/src/uu/date/Cargo.toml @@ -1,7 +1,7 @@ # spell-checker:ignore datetime [package] name = "uu_date" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "date ~ (uutils) display or set the current time" @@ -22,6 +22,8 @@ chrono = { workspace = true } clap = { workspace = true } uucore = { workspace = true } parse_datetime = { workspace = true } +chrono-tz = { workspace = true } +iana-time-zone = { workspace = true } [target.'cfg(unix)'.dependencies] libc = { workspace = true } diff --git a/src/uu/date/src/date.rs b/src/uu/date/src/date.rs index 9c7d865643d..f4d420c3fd2 100644 --- a/src/uu/date/src/date.rs +++ b/src/uu/date/src/date.rs @@ -6,10 +6,12 @@ // spell-checker:ignore (chrono) Datelike Timelike ; (format) DATEFILE MMDDhhmm ; (vars) datetime datetimes use chrono::format::{Item, StrftimeItems}; -use chrono::{DateTime, FixedOffset, Local, Offset, TimeDelta, Utc}; +use chrono::{DateTime, FixedOffset, Local, Offset, TimeDelta, TimeZone, Utc}; #[cfg(windows)] use chrono::{Datelike, Timelike}; +use chrono_tz::{OffsetName, Tz}; use clap::{crate_version, Arg, ArgAction, Command}; +use iana_time_zone::get_timezone; #[cfg(all(unix, not(target_os = "macos"), not(target_os = "redox")))] use libc::{clock_settime, timespec, CLOCK_REALTIME}; use std::fs::File; @@ -103,7 +105,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 +125,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, @@ -272,8 +274,21 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { for date in dates { match date { Ok(date) => { + // TODO - Revisit when chrono 0.5 is released. https://github.com/chronotope/chrono/issues/970 + let tz = match std::env::var("TZ") { + // TODO Support other time zones... + Ok(s) if s == "UTC0" || s.is_empty() => Tz::Etc__UTC, + _ => match get_timezone() { + Ok(tz_str) => tz_str.parse().unwrap(), + Err(_) => Tz::Etc__UTC, + }, + }; + let offset = tz.offset_from_utc_date(&Utc::now().date_naive()); + let tz_abbreviation = offset.abbreviation(); // GNU `date` uses `%N` for nano seconds, however crate::chrono uses `%f` - let format_string = &format_string.replace("%N", "%f"); + let format_string = &format_string + .replace("%N", "%f") + .replace("%Z", tz_abbreviation.unwrap_or("UTC")); // Refuse to pass this string to chrono as it is crashing in this crate if format_string.contains("%#z") { return Err(USimpleError::new( @@ -403,7 +418,7 @@ fn make_format_string(settings: &Settings) -> &str { Rfc3339Format::Ns => "%F %T.%f%:z", }, Format::Custom(ref fmt) => fmt, - Format::Default => "%c", + Format::Default => "%a %b %e %X %Z %Y", } } diff --git a/src/uu/dd/Cargo.toml b/src/uu/dd/Cargo.toml index 5bc117ea7cd..ceb85dcc881 100644 --- a/src/uu/dd/Cargo.toml +++ b/src/uu/dd/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_dd" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "dd ~ (uutils) copy and convert files" diff --git a/src/uu/dd/src/dd.rs b/src/uu/dd/src/dd.rs index 24fab1e2fb3..aaa4684617a 100644 --- a/src/uu/dd/src/dd.rs +++ b/src/uu/dd/src/dd.rs @@ -54,7 +54,7 @@ use nix::{ }; use uucore::display::Quotable; #[cfg(unix)] -use uucore::error::set_exit_code; +use uucore::error::{set_exit_code, USimpleError}; use uucore::error::{FromIo, UResult}; #[cfg(target_os = "linux")] use uucore::show_if_err; @@ -338,11 +338,11 @@ impl<'a> Input<'a> { let mut src = Source::stdin_as_file(); #[cfg(unix)] if let Source::StdinFile(f) = &src { - // GNU compatibility: - // this will check whether stdin points to a folder or not - if f.metadata()?.is_file() && settings.iflags.directory { - show_error!("standard input: not a directory"); - return Err(1.into()); + if settings.iflags.directory && !f.metadata()?.is_dir() { + return Err(USimpleError::new( + 1, + "setting flags for 'standard input': Not a directory", + )); } }; if settings.skip > 0 { @@ -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 8a6fa5a7a37..d0ee2d90b89 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/Cargo.toml b/src/uu/df/Cargo.toml index 3eaff87f38e..7de8028108b 100644 --- a/src/uu/df/Cargo.toml +++ b/src/uu/df/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_df" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "df ~ (uutils) display file system information" diff --git a/src/uu/df/src/df.rs b/src/uu/df/src/df.rs index 517f8a31f1d..092c8381290 100644 --- a/src/uu/df/src/df.rs +++ b/src/uu/df/src/df.rs @@ -27,6 +27,7 @@ use std::path::Path; use crate::blocks::{read_block_size, BlockSize}; use crate::columns::{Column, ColumnError}; use crate::filesystem::Filesystem; +use crate::filesystem::FsError; use crate::table::Table; const ABOUT: &str = help_about!("df.md"); @@ -311,7 +312,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 +331,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 { @@ -352,11 +351,25 @@ fn get_all_filesystems(opt: &Options) -> UResult> { // Convert each `MountInfo` into a `Filesystem`, which contains // both the mount information and usage information. - Ok(mounts - .into_iter() - .filter_map(|m| Filesystem::new(m, None)) - .filter(|fs| opt.show_all_fs || fs.usage.blocks > 0) - .collect()) + #[cfg(not(windows))] + { + let maybe_mount = |m| Filesystem::from_mount(&mounts, &m, None).ok(); + Ok(mounts + .clone() + .into_iter() + .filter_map(maybe_mount) + .filter(|fs| opt.show_all_fs || fs.usage.blocks > 0) + .collect()) + } + #[cfg(windows)] + { + let maybe_mount = |m| Filesystem::from_mount(&m, None).ok(); + Ok(mounts + .into_iter() + .filter_map(maybe_mount) + .filter(|fs| opt.show_all_fs || fs.usage.blocks > 0) + .collect()) + } } /// For each path, get the filesystem that contains that path. @@ -387,17 +400,25 @@ where // both the mount information and usage information. for path in paths { match Filesystem::from_path(&mounts, path) { - Some(fs) => result.push(fs), - None => { - // this happens if specified file system type != file system type of the file - if path.as_ref().exists() { - show!(USimpleError::new(1, "no file systems processed")); - } else { - show!(USimpleError::new( - 1, - format!("{}: No such file or directory", path.as_ref().display()) - )); - } + Ok(fs) => result.push(fs), + Err(FsError::InvalidPath) => { + show!(USimpleError::new( + 1, + format!("{}: No such file or directory", path.as_ref().display()) + )); + } + Err(FsError::MountMissing) => { + show!(USimpleError::new(1, "no file systems processed")); + } + #[cfg(not(windows))] + Err(FsError::OverMounted) => { + show!(USimpleError::new( + 1, + format!( + "cannot access {}: over-mounted by another device", + path.as_ref().quote() + ) + )); } } } diff --git a/src/uu/df/src/filesystem.rs b/src/uu/df/src/filesystem.rs index 5e86cf31781..6f59e2c1027 100644 --- a/src/uu/df/src/filesystem.rs +++ b/src/uu/df/src/filesystem.rs @@ -37,6 +37,33 @@ pub(crate) struct Filesystem { pub usage: FsUsage, } +#[derive(Debug, PartialEq)] +pub(crate) enum FsError { + #[cfg(not(windows))] + OverMounted, + InvalidPath, + MountMissing, +} + +/// Check whether `mount` has been over-mounted. +/// +/// `mount` is considered over-mounted if it there is an element in +/// `mounts` after mount that has the same mount_dir. +#[cfg(not(windows))] +fn is_over_mounted(mounts: &[MountInfo], mount: &MountInfo) -> bool { + let last_mount_for_dir = mounts + .iter() + .filter(|m| m.mount_dir == mount.mount_dir) + .last(); + + if let Some(lmi) = last_mount_for_dir { + lmi.dev_name != mount.dev_name + } else { + // Should be unreachable if `mount` is in `mounts` + false + } +} + /// Find the mount info that best matches a given filesystem path. /// /// This function returns the element of `mounts` on which `path` is @@ -56,14 +83,16 @@ fn mount_info_from_path

( path: P, // This is really only used for testing purposes. canonicalize: bool, -) -> Option<&MountInfo> +) -> Result<&MountInfo, FsError> where P: AsRef, { // TODO Refactor this function with `Stater::find_mount_point()` // in the `stat` crate. let path = if canonicalize { - path.as_ref().canonicalize().ok()? + path.as_ref() + .canonicalize() + .map_err(|_| FsError::InvalidPath)? } else { path.as_ref().to_path_buf() }; @@ -82,12 +111,14 @@ where .find(|m| m.1.eq(&path)) .map(|m| m.0); - maybe_mount_point.or_else(|| { - mounts - .iter() - .filter(|mi| path.starts_with(&mi.mount_dir)) - .max_by_key(|mi| mi.mount_dir.len()) - }) + maybe_mount_point + .or_else(|| { + mounts + .iter() + .filter(|mi| path.starts_with(&mi.mount_dir)) + .max_by_key(|mi| mi.mount_dir.len()) + }) + .ok_or(FsError::MountMissing) } impl Filesystem { @@ -117,6 +148,27 @@ impl Filesystem { }) } + /// Find and create the filesystem from the given mount + /// after checking that the it hasn't been over-mounted + #[cfg(not(windows))] + pub(crate) fn from_mount( + mounts: &[MountInfo], + mount: &MountInfo, + file: Option, + ) -> Result { + if is_over_mounted(mounts, mount) { + Err(FsError::OverMounted) + } else { + Self::new(mount.clone(), file).ok_or(FsError::MountMissing) + } + } + + /// Find and create the filesystem from the given mount. + #[cfg(windows)] + pub(crate) fn from_mount(mount: &MountInfo, file: Option) -> Result { + Self::new(mount.clone(), file).ok_or(FsError::MountMissing) + } + /// Find and create the filesystem that best matches a given path. /// /// This function returns a new `Filesystem` derived from the @@ -133,16 +185,18 @@ impl Filesystem { /// * [`Path::canonicalize`] /// * [`MountInfo::mount_dir`] /// - pub(crate) fn from_path

(mounts: &[MountInfo], path: P) -> Option + pub(crate) fn from_path

(mounts: &[MountInfo], path: P) -> Result where P: AsRef, { let file = path.as_ref().display().to_string(); let canonicalize = true; - let mount_info = mount_info_from_path(mounts, path, canonicalize)?; - // TODO Make it so that we do not need to clone the `mount_info`. - let mount_info = (*mount_info).clone(); - Self::new(mount_info, Some(file)) + + let result = mount_info_from_path(mounts, path, canonicalize); + #[cfg(windows)] + return result.and_then(|mount_info| Self::from_mount(mount_info, Some(file))); + #[cfg(not(windows))] + return result.and_then(|mount_info| Self::from_mount(mounts, mount_info, Some(file))); } } @@ -153,7 +207,7 @@ mod tests { use uucore::fsext::MountInfo; - use crate::filesystem::mount_info_from_path; + use crate::filesystem::{mount_info_from_path, FsError}; // Create a fake `MountInfo` with the given directory name. fn mount_info(mount_dir: &str) -> MountInfo { @@ -183,7 +237,19 @@ mod tests { #[test] fn test_empty_mounts() { - assert!(mount_info_from_path(&[], "/", false).is_none()); + assert_eq!( + mount_info_from_path(&[], "/", false).unwrap_err(), + FsError::MountMissing + ); + } + + #[test] + fn test_bad_path() { + assert_eq!( + // This path better not exist.... + mount_info_from_path(&[], "/non-existent-path", true).unwrap_err(), + FsError::InvalidPath + ); } #[test] @@ -210,13 +276,19 @@ mod tests { #[test] fn test_no_match() { let mounts = [mount_info("/foo")]; - assert!(mount_info_from_path(&mounts, "/bar", false).is_none()); + assert_eq!( + mount_info_from_path(&mounts, "/bar", false).unwrap_err(), + FsError::MountMissing + ); } #[test] fn test_partial_match() { let mounts = [mount_info("/foo/bar")]; - assert!(mount_info_from_path(&mounts, "/foo/baz", false).is_none()); + assert_eq!( + mount_info_from_path(&mounts, "/foo/baz", false).unwrap_err(), + FsError::MountMissing + ); } #[test] @@ -237,4 +309,52 @@ mod tests { assert!(mount_info_eq(actual, &mounts[0])); } } + + #[cfg(not(windows))] + mod over_mount { + use crate::filesystem::{is_over_mounted, Filesystem, FsError}; + use uucore::fsext::MountInfo; + + fn mount_info_with_dev_name(mount_dir: &str, dev_name: Option<&str>) -> MountInfo { + MountInfo { + dev_id: Default::default(), + dev_name: dev_name.map(String::from).unwrap_or_default(), + fs_type: Default::default(), + mount_dir: String::from(mount_dir), + mount_option: Default::default(), + mount_root: Default::default(), + remote: Default::default(), + dummy: Default::default(), + } + } + + #[test] + fn test_over_mount() { + let mount_info1 = mount_info_with_dev_name("/foo", Some("dev_name_1")); + let mount_info2 = mount_info_with_dev_name("/foo", Some("dev_name_2")); + let mounts = [mount_info1, mount_info2]; + assert!(is_over_mounted(&mounts, &mounts[0])); + } + + #[test] + fn test_over_mount_not_over_mounted() { + let mount_info1 = mount_info_with_dev_name("/foo", Some("dev_name_1")); + let mount_info2 = mount_info_with_dev_name("/foo", Some("dev_name_2")); + let mounts = [mount_info1, mount_info2]; + assert!(!is_over_mounted(&mounts, &mounts[1])); + } + + #[test] + fn test_from_mount_over_mounted() { + let mount_info1 = mount_info_with_dev_name("/foo", Some("dev_name_1")); + let mount_info2 = mount_info_with_dev_name("/foo", Some("dev_name_2")); + + let mounts = [mount_info1, mount_info2]; + + assert_eq!( + Filesystem::from_mount(&mounts, &mounts[0], None).unwrap_err(), + FsError::OverMounted + ); + } + } } diff --git a/src/uu/dir/Cargo.toml b/src/uu/dir/Cargo.toml index 73dea05f15a..cfcfc8e9c87 100644 --- a/src/uu/dir/Cargo.toml +++ b/src/uu/dir/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_dir" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "shortcut to ls -C -b" diff --git a/src/uu/dircolors/Cargo.toml b/src/uu/dircolors/Cargo.toml index 4fd6f95371c..085b6a75f9b 100644 --- a/src/uu/dircolors/Cargo.toml +++ b/src/uu/dircolors/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_dircolors" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "dircolors ~ (uutils) display commands to set LS_COLORS" diff --git a/src/uu/dircolors/README.md b/src/uu/dircolors/README.md index ce8aa965f93..62944d4907b 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/dircolors/src/dircolors.rs b/src/uu/dircolors/src/dircolors.rs index faef0683e71..180be5e255f 100644 --- a/src/uu/dircolors/src/dircolors.rs +++ b/src/uu/dircolors/src/dircolors.rs @@ -3,12 +3,11 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (ToDO) clrtoeol dircolors eightbit endcode fnmatch leftcode multihardlink rightcode setenv sgid suid colorterm +// spell-checker:ignore (ToDO) clrtoeol dircolors eightbit endcode fnmatch leftcode multihardlink rightcode setenv sgid suid colorterm disp use std::borrow::Borrow; use std::env; use std::fs::File; -//use std::io::IsTerminal; use std::io::{BufRead, BufReader}; use std::path::Path; @@ -16,7 +15,7 @@ use clap::{crate_version, Arg, ArgAction, Command}; use uucore::colors::{FILE_ATTRIBUTE_CODES, FILE_COLORS, FILE_TYPES, TERMS}; use uucore::display::Quotable; use uucore::error::{UResult, USimpleError, UUsageError}; -use uucore::{help_about, help_section, help_usage}; +use uucore::{format_usage, help_about, help_section, help_usage, parse_glob}; mod options { pub const BOURNE_SHELL: &str = "bourne-shell"; @@ -359,9 +358,6 @@ enum ParseState { Pass, } -use uucore::{format_usage, parse_glob}; - -#[allow(clippy::cognitive_complexity)] fn parse(user_input: T, fmt: &OutputFmt, fp: &str) -> Result where T: IntoIterator, @@ -372,10 +368,12 @@ where result.push_str(&prefix); + // Get environment variables once at the start let term = env::var("TERM").unwrap_or_else(|_| "none".to_owned()); - let term = term.as_str(); + let colorterm = env::var("COLORTERM").unwrap_or_default(); let mut state = ParseState::Global; + let mut saw_colorterm_match = false; for (num, line) in user_input.into_iter().enumerate() { let num = num + 1; @@ -395,52 +393,38 @@ where num )); } + let lower = key.to_lowercase(); - if lower == "term" || lower == "colorterm" { - if term.fnmatch(val) { - state = ParseState::Matched; - } else if state != ParseState::Matched { - state = ParseState::Pass; - } - } else { - if state == ParseState::Matched { - // prevent subsequent mismatched TERM from - // cancelling the input - state = ParseState::Continue; + match lower.as_str() { + "term" => { + if term.fnmatch(val) { + state = ParseState::Matched; + } else if state == ParseState::Global { + state = ParseState::Pass; + } } - if state != ParseState::Pass { - let search_key = lower.as_str(); - - if key.starts_with('.') { - if *fmt == OutputFmt::Display { - result.push_str(format!("\x1b[{val}m*{key}\t{val}\x1b[0m\n").as_str()); - } else { - result.push_str(format!("*{key}={val}:").as_str()); - } - } else if key.starts_with('*') { - if *fmt == OutputFmt::Display { - result.push_str(format!("\x1b[{val}m{key}\t{val}\x1b[0m\n").as_str()); - } else { - result.push_str(format!("{key}={val}:").as_str()); - } - } else if lower == "options" || lower == "color" || lower == "eightbit" { - // Slackware only. Ignore - } else if let Some((_, s)) = FILE_ATTRIBUTE_CODES - .iter() - .find(|&&(key, _)| key == search_key) - { - if *fmt == OutputFmt::Display { - result.push_str(format!("\x1b[{val}m{s}\t{val}\x1b[0m\n").as_str()); - } else { - result.push_str(format!("{s}={val}:").as_str()); - } + "colorterm" => { + // For COLORTERM ?*, only match if COLORTERM is non-empty + let matches = if val == "?*" { + !colorterm.is_empty() } else { - return Err(format!( - "{}:{}: unrecognized keyword {}", - fp.maybe_quote(), - num, - key - )); + colorterm.fnmatch(val) + }; + if matches { + state = ParseState::Matched; + saw_colorterm_match = true; + } else if !saw_colorterm_match && state == ParseState::Global { + state = ParseState::Pass; + } + } + _ => { + if state == ParseState::Matched { + // prevent subsequent mismatched TERM from + // cancelling the input + state = ParseState::Continue; + } + if state != ParseState::Pass { + append_entry(&mut result, fmt, key, &lower, val)?; } } } @@ -455,6 +439,46 @@ where Ok(result) } +fn append_entry( + result: &mut String, + fmt: &OutputFmt, + key: &str, + lower: &str, + val: &str, +) -> Result<(), String> { + if key.starts_with(['.', '*']) { + let entry = if key.starts_with('.') { + format!("*{key}") + } else { + key.to_string() + }; + let disp = if *fmt == OutputFmt::Display { + format!("\x1b[{val}m{entry}\t{val}\x1b[0m\n") + } else { + format!("{entry}={val}:") + }; + result.push_str(&disp); + return Ok(()); + } + + match lower { + "options" | "color" | "eightbit" => Ok(()), // Slackware only, ignore + _ => { + if let Some((_, s)) = FILE_ATTRIBUTE_CODES.iter().find(|&&(key, _)| key == lower) { + let disp = if *fmt == OutputFmt::Display { + format!("\x1b[{val}m{s}\t{val}\x1b[0m\n") + } else { + format!("{s}={val}:") + }; + result.push_str(&disp); + Ok(()) + } else { + Err(format!("unrecognized keyword {key}")) + } + } + } +} + /// Escape single quotes because they are not allowed between single quotes in shell code, and code /// enclosed by single quotes is what is returned by `parse()`. /// diff --git a/src/uu/dirname/Cargo.toml b/src/uu/dirname/Cargo.toml index ac160ffe975..a53930d7e40 100644 --- a/src/uu/dirname/Cargo.toml +++ b/src/uu/dirname/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_dirname" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "dirname ~ (uutils) display parent directory of PATHNAME" diff --git a/src/uu/du/Cargo.toml b/src/uu/du/Cargo.toml index 5b9a707da50..27ec1700a5f 100644 --- a/src/uu/du/Cargo.toml +++ b/src/uu/du/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_du" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "du ~ (uutils) display disk usage" diff --git a/src/uu/du/src/du.rs b/src/uu/du/src/du.rs index a35e9f77e76..2392497a935 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/Cargo.toml b/src/uu/echo/Cargo.toml index faabc121df1..f3a7a6400b7 100644 --- a/src/uu/echo/Cargo.toml +++ b/src/uu/echo/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_echo" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "echo ~ (uutils) display TEXT" diff --git a/src/uu/echo/src/echo.rs b/src/uu/echo/src/echo.rs index 746cdd7c59e..228b5a0c123 100644 --- a/src/uu/echo/src/echo.rs +++ b/src/uu/echo/src/echo.rs @@ -4,8 +4,8 @@ // file that was distributed with this source code. use clap::builder::ValueParser; -use clap::parser::ValuesRef; -use clap::{crate_version, Arg, ArgAction, Command}; +use clap::{crate_version, Arg, ArgAction, ArgMatches, Command}; +use std::env; use std::ffi::{OsStr, OsString}; use std::io::{self, StdoutLock, Write}; use std::iter::Peekable; @@ -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,35 +255,54 @@ 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() +} + +fn collect_args(matches: &ArgMatches) -> Vec { + matches + .get_many::(options::STRING) + .map_or_else(Vec::new, |values| values.cloned().collect()) +} + #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().get_matches_from(args); + let is_posixly_correct = env::var("POSIXLY_CORRECT").is_ok(); - // 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." - // https://www.gnu.org/software/coreutils/manual/html_node/echo-invocation.html + let (args, trailing_newline, escaped) = if is_posixly_correct { + let mut args_iter = args.skip(1).peekable(); - let trailing_newline = !matches.get_flag(options::NO_NEWLINE); - let escaped = matches.get_flag(options::ENABLE_BACKSLASH_ESCAPE); + if args_iter.peek() == Some(&OsString::from("-n")) { + let matches = uu_app().get_matches_from(handle_double_hyphens(args_iter)); + let args = collect_args(&matches); + (args, false, true) + } else { + let args: Vec<_> = args_iter.collect(); + (args, true, true) + } + } else { + let matches = uu_app().get_matches_from(handle_double_hyphens(args.into_iter())); + let trailing_newline = !matches.get_flag(options::NO_NEWLINE); + let escaped = matches.get_flag(options::ENABLE_BACKSLASH_ESCAPE); + let args = collect_args(&matches); + (args, trailing_newline, escaped) + }; let mut stdout_lock = io::stdout().lock(); - - match matches.get_many::(options::STRING) { - Some(arguments_after_options) => { - execute( - &mut stdout_lock, - trailing_newline, - escaped, - arguments_after_options, - )?; - } - None => { - // No strings to print, so just handle newline setting - if trailing_newline { - stdout_lock.write_all(b"\n")?; - } - } - } + execute(&mut stdout_lock, args, trailing_newline, escaped)?; Ok(()) } @@ -350,11 +350,11 @@ pub fn uu_app() -> Command { fn execute( stdout_lock: &mut StdoutLock, + arguments_after_options: Vec, trailing_newline: bool, escaped: bool, - arguments_after_options: ValuesRef<'_, OsString>, ) -> UResult<()> { - for (i, input) in arguments_after_options.enumerate() { + for (i, input) in arguments_after_options.into_iter().enumerate() { let Some(bytes) = bytes_from_os_string(input.as_os_str()) else { return Err(USimpleError::new( 1, diff --git a/src/uu/env/Cargo.toml b/src/uu/env/Cargo.toml index 51a6e817949..9b21ed45ac9 100644 --- a/src/uu/env/Cargo.toml +++ b/src/uu/env/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_env" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "env ~ (uutils) set each NAME to VALUE in the environment and run COMMAND" diff --git a/src/uu/env/src/env.rs b/src/uu/env/src/env.rs index 9e2e56d06c0..b000857a882 100644 --- a/src/uu/env/src/env.rs +++ b/src/uu/env/src/env.rs @@ -370,6 +370,19 @@ impl EnvAppData { self.had_string_argument = true; } _ => { + let arg_str = arg.to_string_lossy(); + + // Short unset option (-u) is not allowed to contain '=' + if arg_str.contains('=') + && arg_str.starts_with("-u") + && !arg_str.starts_with("--") + { + return Err(USimpleError::new( + 125, + format!("cannot unset '{}': Invalid argument", &arg_str[2..]), + )); + } + all_args.push(arg.clone()); } } @@ -526,16 +539,19 @@ impl EnvAppData { } return Err(exit.code().unwrap().into()); } - Err(ref err) - if (err.kind() == io::ErrorKind::NotFound) - || (err.kind() == io::ErrorKind::InvalidInput) => - { - return Err(self.make_error_no_such_file_or_dir(prog.deref())); - } - Err(e) => { - uucore::show_error!("unknown error: {:?}", e); - return Err(126.into()); - } + Err(ref err) => match err.kind() { + io::ErrorKind::NotFound | io::ErrorKind::InvalidInput => { + return Err(self.make_error_no_such_file_or_dir(prog.deref())); + } + io::ErrorKind::PermissionDenied => { + uucore::show_error!("{}: Permission denied", prog.quote()); + return Err(126.into()); + } + _ => { + uucore::show_error!("unknown error: {:?}", err); + return Err(126.into()); + } + }, Ok(_) => (), } Ok(()) diff --git a/src/uu/env/src/string_parser.rs b/src/uu/env/src/string_parser.rs index 0ea4a3c0c60..5cc8d77a12f 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 f225d494572..d08c9f0dcca 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/expand/Cargo.toml b/src/uu/expand/Cargo.toml index fe7a5ba03a9..db3fad7329e 100644 --- a/src/uu/expand/Cargo.toml +++ b/src/uu/expand/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_expand" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "expand ~ (uutils) convert input tabs to spaces" diff --git a/src/uu/expr/Cargo.toml b/src/uu/expr/Cargo.toml index b8596976aec..1abf853d760 100644 --- a/src/uu/expr/Cargo.toml +++ b/src/uu/expr/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_expr" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "expr ~ (uutils) display the value of EXPRESSION" diff --git a/src/uu/factor/Cargo.toml b/src/uu/factor/Cargo.toml index 8b0d35bcb0f..08ff64f57b6 100644 --- a/src/uu/factor/Cargo.toml +++ b/src/uu/factor/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_factor" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "factor ~ (uutils) display the prime factors of each NUMBER" diff --git a/src/uu/false/Cargo.toml b/src/uu/false/Cargo.toml index f8c6d84573a..5c817c754b7 100644 --- a/src/uu/false/Cargo.toml +++ b/src/uu/false/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_false" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "false ~ (uutils) do nothing and fail" diff --git a/src/uu/fmt/Cargo.toml b/src/uu/fmt/Cargo.toml index c71d9a5df32..6522c909f80 100644 --- a/src/uu/fmt/Cargo.toml +++ b/src/uu/fmt/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_fmt" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "fmt ~ (uutils) reformat each paragraph of input" diff --git a/src/uu/fmt/src/fmt.rs b/src/uu/fmt/src/fmt.rs index 007e75dd64e..bb2e1a9780f 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 aa1477ebac0..05d01d1a3ec 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 1ae8ea34f42..8aa18c4c987 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/Cargo.toml b/src/uu/fold/Cargo.toml index 5bfe63ec9c8..cf6d59ad66e 100644 --- a/src/uu/fold/Cargo.toml +++ b/src/uu/fold/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_fold" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "fold ~ (uutils) wrap each line of input" diff --git a/src/uu/fold/src/fold.rs b/src/uu/fold/src/fold.rs index 0223248be27..e17ba21c324 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/groups/Cargo.toml b/src/uu/groups/Cargo.toml index 14a3cb924f1..11055f529a3 100644 --- a/src/uu/groups/Cargo.toml +++ b/src/uu/groups/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_groups" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "groups ~ (uutils) display group memberships for USERNAME" diff --git a/src/uu/hashsum/Cargo.toml b/src/uu/hashsum/Cargo.toml index 42484fabf8f..9ab253bd7b0 100644 --- a/src/uu/hashsum/Cargo.toml +++ b/src/uu/hashsum/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_hashsum" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "hashsum ~ (uutils) display or check input digests" diff --git a/src/uu/hashsum/src/hashsum.rs b/src/uu/hashsum/src/hashsum.rs index 90c8c8adfa7..1d3a758f5ea 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/head/Cargo.toml b/src/uu/head/Cargo.toml index 5e2b977b8a3..3590e11465b 100644 --- a/src/uu/head/Cargo.toml +++ b/src/uu/head/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_head" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "head ~ (uutils) display the first lines of input" @@ -19,6 +19,7 @@ path = "src/head.rs" [dependencies] clap = { workspace = true } memchr = { workspace = true } +thiserror = { workspace = true } uucore = { workspace = true, features = ["ringbuffer", "lines", "fs"] } [[bin]] diff --git a/src/uu/head/src/head.rs b/src/uu/head/src/head.rs index a6bb7b53fc0..52d52f13bba 100644 --- a/src/uu/head/src/head.rs +++ b/src/uu/head/src/head.rs @@ -3,22 +3,21 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (vars) BUFWRITER seekable +// spell-checker:ignore (vars) seekable use clap::{crate_version, Arg, ArgAction, ArgMatches, Command}; use std::ffi::OsString; -use std::io::{self, BufWriter, ErrorKind, Read, Seek, SeekFrom, Write}; +use std::io::{self, ErrorKind, Read, Seek, SeekFrom, Write}; +use std::num::TryFromIntError; +use thiserror::Error; use uucore::display::Quotable; -use uucore::error::{FromIo, UResult, USimpleError}; +use uucore::error::{FromIo, UError, UResult}; use uucore::line_ending::LineEnding; use uucore::lines::lines; use uucore::{format_usage, help_about, help_usage, show}; const BUF_SIZE: usize = 65536; -/// The capacity in bytes for buffered writers. -const BUFWRITER_CAPACITY: usize = 16_384; // 16 kilobytes - const ABOUT: &str = help_about!("head.md"); const USAGE: &str = help_usage!("head.md"); @@ -37,6 +36,36 @@ mod take; use take::take_all_but; use take::take_lines; +#[derive(Error, Debug)] +enum HeadError { + /// Wrapper around `io::Error` + #[error("error reading {name}: {err}")] + Io { name: String, err: io::Error }, + + #[error("parse error: {0}")] + ParseError(String), + + #[error("bad argument encoding")] + BadEncoding, + + #[error("{0}: number of -bytes or -lines is too large")] + NumTooLarge(#[from] TryFromIntError), + + #[error("clap error: {0}")] + Clap(#[from] clap::Error), + + #[error("{0}")] + MatchOption(String), +} + +impl UError for HeadError { + fn code(&self) -> i32 { + 1 + } +} + +type HeadResult = Result; + pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(crate_version!()) @@ -152,7 +181,7 @@ impl Mode { fn arg_iterate<'a>( mut args: impl uucore::Args + 'a, -) -> UResult + 'a>> { +) -> HeadResult + 'a>> { // argv[0] is always present let first = args.next().unwrap(); if let Some(second) = args.next() { @@ -160,22 +189,19 @@ fn arg_iterate<'a>( match parse::parse_obsolete(s) { Some(Ok(iter)) => Ok(Box::new(vec![first].into_iter().chain(iter).chain(args))), Some(Err(e)) => match e { - parse::ParseError::Syntax => Err(USimpleError::new( - 1, - format!("bad argument format: {}", s.quote()), - )), - parse::ParseError::Overflow => Err(USimpleError::new( - 1, - format!( - "invalid argument: {} Value too large for defined datatype", - s.quote() - ), - )), + parse::ParseError::Syntax => Err(HeadError::ParseError(format!( + "bad argument format: {}", + s.quote() + ))), + parse::ParseError::Overflow => Err(HeadError::ParseError(format!( + "invalid argument: {} Value too large for defined datatype", + s.quote() + ))), }, None => Ok(Box::new(vec![first, second].into_iter().chain(args))), } } else { - Err(USimpleError::new(1, "bad argument encoding".to_owned())) + Err(HeadError::BadEncoding) } } else { Ok(Box::new(vec![first].into_iter())) @@ -226,6 +252,11 @@ where io::copy(&mut reader, &mut stdout)?; + // Make sure we finish writing everything to the target before + // exiting. Otherwise, when Rust is implicitly flushing, any + // error will be silently ignored. + stdout.flush()?; + Ok(()) } @@ -234,11 +265,14 @@ fn read_n_lines(input: &mut impl std::io::BufRead, n: u64, separator: u8) -> std let mut reader = take_lines(input, n, separator); // Write those bytes to `stdout`. - let stdout = std::io::stdout(); - let stdout = stdout.lock(); - let mut writer = BufWriter::with_capacity(BUFWRITER_CAPACITY, stdout); + let mut stdout = std::io::stdout(); + + io::copy(&mut reader, &mut stdout)?; - io::copy(&mut reader, &mut writer)?; + // Make sure we finish writing everything to the target before + // exiting. Otherwise, when Rust is implicitly flushing, any + // error will be silently ignored. + stdout.flush()?; Ok(()) } @@ -247,15 +281,13 @@ fn catch_too_large_numbers_in_backwards_bytes_or_lines(n: u64) -> Option match usize::try_from(n) { Ok(value) => Some(value), Err(e) => { - show!(USimpleError::new( - 1, - format!("{e}: number of -bytes or -lines is too large") - )); + show!(HeadError::NumTooLarge(e)); None } } } +/// Print to stdout all but the last `n` bytes from the given reader. fn read_but_last_n_bytes(input: &mut impl std::io::BufRead, n: u64) -> std::io::Result<()> { if n == 0 { //prints everything @@ -285,7 +317,7 @@ fn read_but_last_n_bytes(input: &mut impl std::io::BufRead, n: u64) -> std::io:: if total_read <= n { // Fill the ring buffer without exceeding n bytes - let overflow = total_read - n; + let overflow = n - total_read; ring_buffer.extend_from_slice(&buffer[..read - overflow]); } else { // Write the ring buffer and the part of the buffer that exceeds n @@ -510,16 +542,17 @@ fn uu_head(options: &HeadOptions) -> UResult<()> { head_file(&mut file, options) } }; - if res.is_err() { + if let Err(e) = res { let name = if file.as_str() == "-" { "standard input" } else { file }; - show!(USimpleError::new( - 1, - format!("error reading {name}: Input/output error") - )); + return Err(HeadError::Io { + name: name.to_string(), + err: e, + } + .into()); } first = false; } @@ -536,7 +569,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let args = match HeadOptions::get_from(&matches) { Ok(o) => o, Err(s) => { - return Err(USimpleError::new(1, s)); + return Err(HeadError::MatchOption(s).into()); } }; uu_head(&args) diff --git a/src/uu/hostid/Cargo.toml b/src/uu/hostid/Cargo.toml index e667e76ca5a..4e853ac8edd 100644 --- a/src/uu/hostid/Cargo.toml +++ b/src/uu/hostid/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_hostid" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "hostid ~ (uutils) display the numeric identifier of the current host" diff --git a/src/uu/hostname/Cargo.toml b/src/uu/hostname/Cargo.toml index 716d76f6902..0fa58481717 100644 --- a/src/uu/hostname/Cargo.toml +++ b/src/uu/hostname/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_hostname" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "hostname ~ (uutils) display or set the host name of the current host" diff --git a/src/uu/id/Cargo.toml b/src/uu/id/Cargo.toml index 8be08ee8060..575808042c7 100644 --- a/src/uu/id/Cargo.toml +++ b/src/uu/id/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_id" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "id ~ (uutils) display user and group information for USER" diff --git a/src/uu/install/Cargo.toml b/src/uu/install/Cargo.toml index 809a1dd687a..26507023806 100644 --- a/src/uu/install/Cargo.toml +++ b/src/uu/install/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_install" -version = "0.0.28" +version = "0.0.29" authors = ["Ben Eills ", "uutils developers"] license = "MIT" description = "install ~ (uutils) copy files from SOURCE to DESTINATION (with specified attributes)" @@ -23,6 +23,7 @@ file_diff = { workspace = true } libc = { workspace = true } uucore = { workspace = true, features = [ "backup-control", + "buf-copy", "fs", "mode", "perms", diff --git a/src/uu/install/src/install.rs b/src/uu/install/src/install.rs index 331a50f6741..cf810937794 100644 --- a/src/uu/install/src/install.rs +++ b/src/uu/install/src/install.rs @@ -12,14 +12,12 @@ use file_diff::diff; use filetime::{set_file_times, FileTime}; use std::error::Error; use std::fmt::{Debug, Display}; -use std::fs; use std::fs::File; -use std::os::unix::fs::MetadataExt; -#[cfg(unix)] -use std::os::unix::prelude::OsStrExt; +use std::fs::{self, metadata}; use std::path::{Path, PathBuf, MAIN_SEPARATOR}; use std::process; use uucore::backup_control::{self, BackupMode}; +use uucore::buf_copy::copy_stream; use uucore::display::Quotable; use uucore::entries::{grp2gid, usr2uid}; use uucore::error::{FromIo, UError, UIoError, UResult, UUsageError}; @@ -29,6 +27,11 @@ use uucore::perms::{wrap_chown, Verbosity, VerbosityLevel}; use uucore::process::{getegid, geteuid}; use uucore::{format_usage, help_about, help_usage, show, show_error, show_if_err, uio_error}; +#[cfg(unix)] +use std::os::unix::fs::{FileTypeExt, MetadataExt}; +#[cfg(unix)] +use std::os::unix::prelude::OsStrExt; + const DEFAULT_MODE: u32 = 0o755; const DEFAULT_STRIP_PROGRAM: &str = "strip"; @@ -736,7 +739,24 @@ fn perform_backup(to: &Path, b: &Behavior) -> UResult> { } } -/// Copy a file from one path to another. +/// Copy a non-special file using std::fs::copy. +/// +/// # Parameters +/// * `from` - The source file path. +/// * `to` - The destination file path. +/// +/// # Returns +/// +/// Returns an empty Result or an error in case of failure. +fn copy_normal_file(from: &Path, to: &Path) -> UResult<()> { + if let Err(err) = fs::copy(from, to) { + return Err(InstallError::InstallFailed(from.to_path_buf(), to.to_path_buf(), err).into()); + } + Ok(()) +} + +/// Copy a file from one path to another. Handles the certain cases of special +/// files (e.g character specials). /// /// # Parameters /// @@ -760,18 +780,26 @@ fn copy_file(from: &Path, to: &Path) -> UResult<()> { } } - if from.as_os_str() == "/dev/null" { - /* workaround a limitation of fs::copy - * https://github.com/rust-lang/rust/issues/79390 - */ - if let Err(err) = File::create(to) { + let ft = match metadata(from) { + Ok(ft) => ft.file_type(), + Err(err) => { return Err( InstallError::InstallFailed(from.to_path_buf(), to.to_path_buf(), err).into(), ); } - } else if let Err(err) = fs::copy(from, to) { - return Err(InstallError::InstallFailed(from.to_path_buf(), to.to_path_buf(), err).into()); + }; + + // Stream-based copying to get around the limitations of std::fs::copy + #[cfg(unix)] + if ft.is_char_device() || ft.is_block_device() || ft.is_fifo() { + let mut handle = File::open(from)?; + let mut dest = File::create(to)?; + copy_stream(&mut handle, &mut dest)?; + return Ok(()); } + + copy_normal_file(from, to)?; + Ok(()) } diff --git a/src/uu/join/Cargo.toml b/src/uu/join/Cargo.toml index 322fcbf6bd1..a19b6818f04 100644 --- a/src/uu/join/Cargo.toml +++ b/src/uu/join/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_join" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "join ~ (uutils) merge lines from inputs with matching join fields" diff --git a/src/uu/join/src/join.rs b/src/uu/join/src/join.rs index e7bc7da6963..01e1b40fc4a 100644 --- a/src/uu/join/src/join.rs +++ b/src/uu/join/src/join.rs @@ -20,7 +20,7 @@ use std::os::unix::ffi::OsStrExt; use uucore::display::Quotable; use uucore::error::{set_exit_code, FromIo, UError, UResult, USimpleError}; use uucore::line_ending::LineEnding; -use uucore::{crash_if_err, format_usage, help_about, help_usage}; +use uucore::{format_usage, help_about, help_usage}; const ABOUT: &str = help_about!("join.md"); const USAGE: &str = help_usage!("join.md"); @@ -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; @@ -587,15 +587,19 @@ impl<'a> State<'a> { !self.seq.is_empty() } - fn initialize(&mut self, read_sep: &Sep, autoformat: bool) -> usize { - if let Some(line) = crash_if_err!(1, self.read_line(read_sep)) { + fn initialize( + &mut self, + read_sep: &Sep, + autoformat: bool, + ) -> std::io::Result { + if let Some(line) = self.read_line(read_sep)? { self.seq.push(line); if autoformat { - return self.seq[0].field_ranges.len(); + return Ok(self.seq[0].field_ranges.len()); } } - 0 + Ok(0) } fn finalize( @@ -1008,20 +1012,21 @@ fn exec(file1: &str, file2: &str, settings: Settings, sep: Sep) let format = if settings.autoformat { let mut format = vec![Spec::Key]; - let mut initialize = |state: &mut State| { - let max_fields = state.initialize(&sep, settings.autoformat); + let mut initialize = |state: &mut State| -> UResult<()> { + let max_fields = state.initialize(&sep, settings.autoformat)?; for i in 0..max_fields { if i != state.key { format.push(Spec::Field(state.file_num, i)); } } + Ok(()) }; - initialize(&mut state1); - initialize(&mut state2); + initialize(&mut state1)?; + initialize(&mut state2)?; format } else { - state1.initialize(&sep, settings.autoformat); - state2.initialize(&sep, settings.autoformat); + state1.initialize(&sep, settings.autoformat)?; + state2.initialize(&sep, settings.autoformat)?; settings.format }; diff --git a/src/uu/kill/Cargo.toml b/src/uu/kill/Cargo.toml index 22b05268ec1..aa7cb4749d3 100644 --- a/src/uu/kill/Cargo.toml +++ b/src/uu/kill/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_kill" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "kill ~ (uutils) send a signal to a process" diff --git a/src/uu/kill/src/kill.rs b/src/uu/kill/src/kill.rs index b5bcc0d7c0c..1dc3526538d 100644 --- a/src/uu/kill/src/kill.rs +++ b/src/uu/kill/src/kill.rs @@ -122,9 +122,11 @@ pub fn uu_app() -> Command { .arg( Arg::new(options::SIGNAL) .short('s') + .short_alias('n') // For bash compatibility, like in GNU coreutils .long(options::SIGNAL) .value_name("signal") - .help("Sends given signal instead of SIGTERM"), + .help("Sends given signal instead of SIGTERM") + .conflicts_with_all([options::LIST, options::TABLE]), ) .arg( Arg::new(options::PIDS_OR_SIGNALS) diff --git a/src/uu/link/Cargo.toml b/src/uu/link/Cargo.toml index 4baf6adcfcd..b8c5df3618e 100644 --- a/src/uu/link/Cargo.toml +++ b/src/uu/link/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_link" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "link ~ (uutils) create a hard (file system) link to FILE" diff --git a/src/uu/ln/Cargo.toml b/src/uu/ln/Cargo.toml index 488262a417c..5b82e211e0a 100644 --- a/src/uu/ln/Cargo.toml +++ b/src/uu/ln/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_ln" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "ln ~ (uutils) create a (file system) link to TARGET" diff --git a/src/uu/logname/Cargo.toml b/src/uu/logname/Cargo.toml index deb1d1229a1..d0bf9a8d6ab 100644 --- a/src/uu/logname/Cargo.toml +++ b/src/uu/logname/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_logname" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "logname ~ (uutils) display the login name of the current user" diff --git a/src/uu/ls/Cargo.toml b/src/uu/ls/Cargo.toml index de50b4c924b..17cef9b8aa4 100644 --- a/src/uu/ls/Cargo.toml +++ b/src/uu/ls/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_ls" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "ls ~ (uutils) display directory contents" diff --git a/src/uu/ls/src/colors.rs b/src/uu/ls/src/colors.rs index 6c580d18a7a..2a1eb254e7c 100644 --- a/src/uu/ls/src/colors.rs +++ b/src/uu/ls/src/colors.rs @@ -5,6 +5,7 @@ use super::get_metadata_with_deref_opt; use super::PathData; use lscolors::{Indicator, LsColors, Style}; +use std::ffi::OsString; use std::fs::{DirEntry, Metadata}; use std::io::{BufWriter, Stdout}; @@ -31,9 +32,9 @@ impl<'a> StyleManager<'a> { pub(crate) fn apply_style( &mut self, new_style: Option<&Style>, - name: &str, + name: OsString, wrap: bool, - ) -> String { + ) -> OsString { let mut style_code = String::new(); let mut force_suffix_reset: bool = false; @@ -50,14 +51,14 @@ impl<'a> StyleManager<'a> { // normal style is to be printed we could skip printing new color // codes if !self.is_current_style(new_style) { - style_code.push_str(&self.reset(!self.initial_reset_is_done)); + style_code.push_str(self.reset(!self.initial_reset_is_done)); style_code.push_str(&self.get_style_code(new_style)); } } // if new style is None and current style is Normal we should reset it else if matches!(self.get_normal_style().copied(), Some(norm_style) if self.is_current_style(&norm_style)) { - style_code.push_str(&self.reset(false)); + style_code.push_str(self.reset(false)); // even though this is an unnecessary reset for gnu compatibility we allow it here force_suffix_reset = true; } @@ -69,16 +70,17 @@ impl<'a> StyleManager<'a> { // till the end of line let clear_to_eol = if wrap { "\x1b[K" } else { "" }; - format!( - "{style_code}{name}{}{clear_to_eol}", - self.reset(force_suffix_reset), - ) + let mut ret: OsString = style_code.into(); + ret.push(name); + ret.push(self.reset(force_suffix_reset)); + ret.push(clear_to_eol); + ret } /// Resets the current style and returns the default ANSI reset code to /// reset all text formatting attributes. If `force` is true, the reset is /// done even if the reset has been applied before. - pub(crate) fn reset(&mut self, force: bool) -> String { + pub(crate) fn reset(&mut self, force: bool) -> &str { // todo: // We need to use style from `Indicator::Reset` but as of now ls colors // uses a fallback mechanism and because of that if `Indicator::Reset` @@ -87,9 +89,9 @@ impl<'a> StyleManager<'a> { if self.current_style.is_some() || force { self.initial_reset_is_done = true; self.current_style = None; - return "\x1b[0m".to_string(); + return "\x1b[0m"; } - String::new() + "" } pub(crate) fn get_style_code(&mut self, new_style: &Style) -> String { @@ -124,9 +126,9 @@ impl<'a> StyleManager<'a> { &mut self, path: &PathData, md_option: Option<&Metadata>, - name: &str, + name: OsString, wrap: bool, - ) -> String { + ) -> OsString { let style = self .colors .style_for_path_with_metadata(&path.p_buf, md_option); @@ -136,9 +138,9 @@ impl<'a> StyleManager<'a> { pub(crate) fn apply_style_based_on_dir_entry( &mut self, dir_entry: &DirEntry, - name: &str, + name: OsString, wrap: bool, - ) -> String { + ) -> OsString { let style = self.colors.style_for(dir_entry); self.apply_style(style, name, wrap) } @@ -149,13 +151,33 @@ impl<'a> StyleManager<'a> { /// unnecessary calls to stat() /// and manages the symlink errors pub(crate) fn color_name( - name: &str, + name: OsString, path: &PathData, style_manager: &mut StyleManager, out: &mut BufWriter, target_symlink: Option<&PathData>, wrap: bool, -) -> String { +) -> OsString { + // 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/dired.rs b/src/uu/ls/src/dired.rs index 0faec2c92f9..69122ccd08b 100644 --- a/src/uu/ls/src/dired.rs +++ b/src/uu/ls/src/dired.rs @@ -183,8 +183,7 @@ pub fn update_positions(dired: &mut DiredOutput, start: usize, end: usize) { /// we don't use clap here because we need to know if the argument is present /// as it can be overridden by --hyperlink pub fn is_dired_arg_present() -> bool { - let args: Vec = std::env::args().collect(); - args.iter().any(|x| x == "--dired" || x == "-D") + std::env::args_os().any(|x| x == "--dired" || x == "-D") } #[cfg(test)] diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index f4e34714704..994eabc21b6 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,12 +55,13 @@ 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, escape_name, QuotingStyle}; use uucore::{ display::Quotable, error::{set_exit_code, UError, UResult}, format_usage, fs::display_permissions, + os_str_as_bytes_lossy, parse_size::parse_size_u64, shortcut_value_parser::ShortcutValueParser, version_cmp::version_cmp, @@ -536,6 +537,7 @@ fn extract_time(options: &clap::ArgMatches) -> Time { match field.as_str() { "ctime" | "status" => Time::Change, "access" | "atime" | "use" => Time::Access, + "mtime" | "modification" => Time::Modification, "birth" | "creation" => Time::Birth, // below should never happen as clap already restricts the values. _ => unreachable!("Invalid field for --time"), @@ -1442,12 +1444,14 @@ pub fn uu_app() -> Command { "Show time in :\n\ \taccess time (-u): atime, access, use;\n\ \tchange time (-t): ctime, status.\n\ + \tmodification time: mtime, modification.\n\ \tbirth time: birth, creation;", ) .value_name("field") .value_parser(ShortcutValueParser::new([ PossibleValue::new("atime").alias("access").alias("use"), PossibleValue::new("ctime").alias("status"), + PossibleValue::new("mtime").alias("modification"), PossibleValue::new("birth").alias("creation"), ])) .hide_possible_values(true) @@ -2047,8 +2051,13 @@ impl PathData { /// dir1: <- This as well /// 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); +fn show_dir_name( + path_data: &PathData, + out: &mut BufWriter, + config: &Config, +) -> std::io::Result<()> { + let escaped_name = + quoting_style::escape_dir_name(path_data.p_buf.as_os_str(), &config.quoting_style); let name = if config.hyperlink && !config.dired { create_hyperlink(&escaped_name, path_data) @@ -2056,7 +2065,8 @@ fn show_dir_name(path_data: &PathData, out: &mut BufWriter, config: &Con escaped_name }; - write!(out, "{name}:").unwrap(); + write_os_str(out, &name)?; + write!(out, ":") } #[allow(clippy::cognitive_complexity)] @@ -2133,7 +2143,7 @@ pub fn list(locs: Vec<&Path>, config: &Config) -> UResult<()> { if config.dired { dired::indent(&mut out)?; } - show_dir_name(path_data, &mut out, config); + show_dir_name(path_data, &mut out, config)?; writeln!(out)?; if config.dired { // First directory displayed @@ -2145,7 +2155,7 @@ pub fn list(locs: Vec<&Path>, config: &Config) -> UResult<()> { } } else { writeln!(out)?; - show_dir_name(path_data, &mut out, config); + show_dir_name(path_data, &mut out, config)?; writeln!(out)?; } } @@ -2262,7 +2272,11 @@ fn should_display(entry: &DirEntry, config: &Config) -> bool { case_sensitive: true, }; let file_name = entry.file_name(); - // If the decoding fails, still show an incorrect rendering + // If the decoding fails, still match best we can + // FIXME: use OsStrings or Paths once we have a glob crate that supports it: + // https://github.com/rust-lang/glob/issues/23 + // https://github.com/rust-lang/glob/issues/78 + // https://github.com/BurntSushi/ripgrep/issues/1250 let file_name = match file_name.to_str() { Some(s) => s.to_string(), None => file_name.to_string_lossy().into_owned(), @@ -2374,7 +2388,7 @@ fn enter_directory( dired::add_dir_name(dired, dir_name_size); } - show_dir_name(e, out, config); + show_dir_name(e, out, config)?; writeln!(out)?; enter_directory( e, @@ -2514,7 +2528,7 @@ fn display_items( let quoted = items.iter().any(|item| { let name = escape_name(&item.display_name, &config.quoting_style); - name.starts_with('\'') + os_str_starts_with(&name, b"'") }); if config.format == Format::Long { @@ -2588,19 +2602,20 @@ fn display_items( Format::Commas => { let mut current_col = 0; if let Some(name) = names.next() { - write!(out, "{name}")?; - current_col = ansi_width(&name) as u16 + 2; + write_os_str(out, &name)?; + current_col = ansi_width(&name.to_string_lossy()) as u16 + 2; } for name in names { - let name_width = ansi_width(&name) as u16; + let name_width = ansi_width(&name.to_string_lossy()) as u16; // If the width is 0 we print one single line if config.width != 0 && current_col + name_width + 1 > config.width { current_col = name_width + 2; - write!(out, ",\n{name}")?; + writeln!(out, ",")?; } else { current_col += name_width + 2; - write!(out, ", {name}")?; + write!(out, ", ")?; } + write_os_str(out, &name)?; } // Current col is never zero again if names have been printed. // So we print a newline. @@ -2610,7 +2625,8 @@ fn display_items( } _ => { for name in names { - write!(out, "{}{}", name, config.line_ending)?; + write_os_str(out, &name)?; + write!(out, "{}", config.line_ending)?; } } }; @@ -2644,7 +2660,7 @@ fn get_block_size(md: &Metadata, config: &Config) -> u64 { } fn display_grid( - names: impl Iterator, + names: impl Iterator, width: u16, direction: Direction, out: &mut BufWriter, @@ -2658,13 +2674,13 @@ fn display_grid( write!(out, " ")?; } printed_something = true; - write!(out, "{name}")?; + write_os_str(out, &name)?; } if printed_something { writeln!(out)?; } } else { - let names: Vec = if quoted { + let names: Vec<_> = if quoted { // In case some names are quoted, GNU adds a space before each // entry that does not start with a quote to make it prettier // on multiline. @@ -2679,10 +2695,12 @@ fn display_grid( // ``` names .map(|n| { - if n.starts_with('\'') || n.starts_with('"') { + if os_str_starts_with(&n, b"'") || os_str_starts_with(&n, b"\"") { n } else { - format!(" {n}") + let mut ret: OsString = " ".into(); + ret.push(n); + ret } }) .collect() @@ -2690,6 +2708,12 @@ fn display_grid( names.collect() }; + // FIXME: the Grid crate only supports &str, so can't display raw bytes + let names: Vec<_> = names + .into_iter() + .map(|s| s.to_string_lossy().into_owned()) + .collect(); + // Determine whether to use tabs for separation based on whether any entry ends with '/'. // If any entry ends with '/', it indicates that the -F flag is likely used to classify directories. let use_tabs = names.iter().any(|name| name.ends_with('/')); @@ -2751,14 +2775,14 @@ fn display_item_long( style_manager: &mut Option, quoted: bool, ) -> UResult<()> { - let mut output_display: String = String::new(); + let mut output_display: Vec = vec![]; // apply normal color to non filename outputs if let Some(style_manager) = style_manager { write!(output_display, "{}", style_manager.apply_normal()).unwrap(); } if config.dired { - output_display += " "; + output_display.extend(b" "); } if let Some(md) = item.get_metadata(out) { #[cfg(any(not(unix), target_os = "android", target_os = "macos"))] @@ -2865,11 +2889,13 @@ fn display_item_long( String::new(), out, style_manager, - ansi_width(&output_display), + ansi_width(&String::from_utf8_lossy(&output_display)), ); - let displayed_item = if quoted && !item_name.starts_with('\'') { - format!(" {item_name}") + let displayed_item = if quoted && !os_str_starts_with(&item_name, b"'") { + let mut ret: OsString = " ".into(); + ret.push(item_name); + ret } else { item_name }; @@ -2882,7 +2908,8 @@ fn display_item_long( ); dired::update_positions(dired, start, end); } - write!(output_display, "{}{}", displayed_item, config.line_ending).unwrap(); + write_os_str(&mut output_display, &displayed_item)?; + write!(output_display, "{}", config.line_ending)?; } else { #[cfg(unix)] let leading_char = { @@ -2962,7 +2989,7 @@ fn display_item_long( String::new(), out, style_manager, - ansi_width(&output_display), + ansi_width(&String::from_utf8_lossy(&output_display)), ); let date_len = 12; @@ -2978,12 +3005,13 @@ fn display_item_long( dired::calculate_and_update_positions( dired, output_display.len(), - displayed_item.trim().len(), + displayed_item.to_string_lossy().trim().len(), ); } - write!(output_display, "{}{}", displayed_item, config.line_ending).unwrap(); + write_os_str(&mut output_display, &displayed_item)?; + write!(output_display, "{}", config.line_ending)?; } - write!(out, "{output_display}")?; + out.write_all(&output_display)?; Ok(()) } @@ -3002,7 +3030,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 { @@ -3225,7 +3252,7 @@ fn display_item_name( out: &mut BufWriter, style_manager: &mut Option, current_column: usize, -) -> String { +) -> OsString { // This is our return value. We start by `&path.display_name` and modify it along the way. let mut name = escape_name(&path.display_name, &config.quoting_style); @@ -3237,11 +3264,14 @@ fn display_item_name( } if let Some(style_manager) = style_manager { - name = color_name(&name, path, style_manager, out, None, is_wrap(name.len())); + let len = name.len(); + name = color_name(name, path, style_manager, out, None, is_wrap(len)); } if config.format != Format::Long && !more_info.is_empty() { - name = more_info + &name; + let old_name = name; + name = more_info.into(); + name.push(&old_name); } if config.indicator_style != IndicatorStyle::None { @@ -3267,7 +3297,7 @@ fn display_item_name( }; if let Some(c) = char_opt { - name.push(c); + name.push(OsStr::new(&c.to_string())); } } @@ -3278,7 +3308,7 @@ fn display_item_name( { match path.p_buf.read_link() { Ok(target) => { - name.push_str(" -> "); + name.push(" -> "); // We might as well color the symlink output after the arrow. // This makes extra system calls, but provides important information that @@ -3306,10 +3336,10 @@ fn display_item_name( ) .is_err() { - name.push_str(&path.p_buf.read_link().unwrap().to_string_lossy()); + name.push(path.p_buf.read_link().unwrap()); } else { - name.push_str(&color_name( - &escape_name(target.as_os_str(), &config.quoting_style), + name.push(color_name( + escape_name(target.as_os_str(), &config.quoting_style), path, style_manager, out, @@ -3320,7 +3350,7 @@ fn display_item_name( } else { // If no coloring is required, we just use target as is. // Apply the right quoting - name.push_str(&escape_name(target.as_os_str(), &config.quoting_style)); + name.push(escape_name(target.as_os_str(), &config.quoting_style)); } } Err(err) => { @@ -3338,14 +3368,16 @@ fn display_item_name( } else { pad_left(&path.security_context, pad_count) }; - name = format!("{security_context} {name}"); + let old_name = name; + name = format!("{security_context} ").into(); + name.push(old_name); } } name } -fn create_hyperlink(name: &str, path: &PathData) -> String { +fn create_hyperlink(name: &OsStr, path: &PathData) -> OsString { let hostname = hostname::get().unwrap_or_else(|_| OsString::from("")); let hostname = hostname.to_string_lossy(); @@ -3370,7 +3402,10 @@ fn create_hyperlink(name: &str, path: &PathData) -> String { .collect(); // \x1b = ESC, \x07 = BEL - format!("\x1b]8;;file://{hostname}{absolute_path}\x07{name}\x1b]8;;\x07") + let mut ret: OsString = format!("\x1b]8;;file://{hostname}{absolute_path}\x07").into(); + ret.push(name); + ret.push("\x1b]8;;\x07"); + ret } #[cfg(not(unix))] @@ -3542,3 +3577,11 @@ fn calculate_padding_collection( padding_collections } + +fn os_str_starts_with(haystack: &OsStr, needle: &[u8]) -> bool { + os_str_as_bytes_lossy(haystack).starts_with(needle) +} + +fn write_os_str(writer: &mut W, string: &OsStr) -> std::io::Result<()> { + writer.write_all(&os_str_as_bytes_lossy(string)) +} diff --git a/src/uu/mkdir/Cargo.toml b/src/uu/mkdir/Cargo.toml index 6fbce879a8b..80871b11544 100644 --- a/src/uu/mkdir/Cargo.toml +++ b/src/uu/mkdir/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_mkdir" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "mkdir ~ (uutils) create DIRECTORY" diff --git a/src/uu/mkfifo/Cargo.toml b/src/uu/mkfifo/Cargo.toml index 960ed601d16..e2605ae1de9 100644 --- a/src/uu/mkfifo/Cargo.toml +++ b/src/uu/mkfifo/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_mkfifo" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "mkfifo ~ (uutils) create FIFOs (named pipes)" @@ -19,7 +19,7 @@ path = "src/mkfifo.rs" [dependencies] clap = { workspace = true } libc = { workspace = true } -uucore = { workspace = true } +uucore = { workspace = true, features = ["fs", "mode"] } [[bin]] name = "mkfifo" diff --git a/src/uu/mkfifo/src/mkfifo.rs b/src/uu/mkfifo/src/mkfifo.rs index 9320f76ed04..01fc5dc1e60 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/mknod/Cargo.toml b/src/uu/mknod/Cargo.toml index 9177fe4f466..02e3f4cab8a 100644 --- a/src/uu/mknod/Cargo.toml +++ b/src/uu/mknod/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_mknod" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "mknod ~ (uutils) create special file NAME of TYPE" diff --git a/src/uu/mktemp/Cargo.toml b/src/uu/mktemp/Cargo.toml index 0bcd6ac5187..60d0d28997b 100644 --- a/src/uu/mktemp/Cargo.toml +++ b/src/uu/mktemp/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_mktemp" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "mktemp ~ (uutils) create and display a temporary file or directory from TEMPLATE" diff --git a/src/uu/more/Cargo.toml b/src/uu/more/Cargo.toml index 5af95afe65e..ef0bb8de1e7 100644 --- a/src/uu/more/Cargo.toml +++ b/src/uu/more/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_more" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "more ~ (uutils) input perusal filter" diff --git a/src/uu/more/src/more.rs b/src/uu/more/src/more.rs index 0b8c838f29d..61d9b2adbac 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/Cargo.toml b/src/uu/mv/Cargo.toml index 7e8ccc16c32..4982edd8050 100644 --- a/src/uu/mv/Cargo.toml +++ b/src/uu/mv/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_mv" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "mv ~ (uutils) move (rename) SOURCE to DESTINATION" diff --git a/src/uu/mv/src/error.rs b/src/uu/mv/src/error.rs index f989d4e1332..6daa8188ec1 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 c57f2527e1f..675982bacba 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/nice/Cargo.toml b/src/uu/nice/Cargo.toml index 3d231aff6c0..afcce849e37 100644 --- a/src/uu/nice/Cargo.toml +++ b/src/uu/nice/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_nice" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "nice ~ (uutils) run PROGRAM with modified scheduling priority" diff --git a/src/uu/nl/Cargo.toml b/src/uu/nl/Cargo.toml index 7f3091d942c..d82793761ca 100644 --- a/src/uu/nl/Cargo.toml +++ b/src/uu/nl/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_nl" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "nl ~ (uutils) display input with added line numbers" diff --git a/src/uu/nohup/Cargo.toml b/src/uu/nohup/Cargo.toml index 55660a99240..df324856107 100644 --- a/src/uu/nohup/Cargo.toml +++ b/src/uu/nohup/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_nohup" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "nohup ~ (uutils) run COMMAND, ignoring hangup signals" diff --git a/src/uu/nproc/Cargo.toml b/src/uu/nproc/Cargo.toml index ebcf8b6d2a2..5b65d445f70 100644 --- a/src/uu/nproc/Cargo.toml +++ b/src/uu/nproc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_nproc" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "nproc ~ (uutils) display the number of processing units available" diff --git a/src/uu/numfmt/Cargo.toml b/src/uu/numfmt/Cargo.toml index 1ae42b5d5cf..1313a234e32 100644 --- a/src/uu/numfmt/Cargo.toml +++ b/src/uu/numfmt/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_numfmt" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "numfmt ~ (uutils) reformat NUMBER" diff --git a/src/uu/od/Cargo.toml b/src/uu/od/Cargo.toml index 2ca1678abf4..c713f121f3e 100644 --- a/src/uu/od/Cargo.toml +++ b/src/uu/od/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_od" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "od ~ (uutils) display formatted representation of input" diff --git a/src/uu/od/src/inputdecoder.rs b/src/uu/od/src/inputdecoder.rs index 62117d54608..44ad2922843 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 813ef029f37..34cd251ac78 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/Cargo.toml b/src/uu/paste/Cargo.toml index f8ea87965a7..dc18d3124b1 100644 --- a/src/uu/paste/Cargo.toml +++ b/src/uu/paste/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_paste" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "paste ~ (uutils) merge lines from inputs" diff --git a/src/uu/paste/src/paste.rs b/src/uu/paste/src/paste.rs index 9d26197813b..456639ba972 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/pathchk/Cargo.toml b/src/uu/pathchk/Cargo.toml index 24005d30a49..25904bfdbf4 100644 --- a/src/uu/pathchk/Cargo.toml +++ b/src/uu/pathchk/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_pathchk" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "pathchk ~ (uutils) diagnose invalid or non-portable PATHNAME" diff --git a/src/uu/pinky/Cargo.toml b/src/uu/pinky/Cargo.toml index cb64288c664..4af298339d4 100644 --- a/src/uu/pinky/Cargo.toml +++ b/src/uu/pinky/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_pinky" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "pinky ~ (uutils) display user information" diff --git a/src/uu/pr/Cargo.toml b/src/uu/pr/Cargo.toml index 04aec10307f..2e245569e8f 100644 --- a/src/uu/pr/Cargo.toml +++ b/src/uu/pr/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_pr" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "pr ~ (uutils) convert text files for printing" diff --git a/src/uu/printenv/Cargo.toml b/src/uu/printenv/Cargo.toml index 65abcdd3381..e5e07ced3c2 100644 --- a/src/uu/printenv/Cargo.toml +++ b/src/uu/printenv/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_printenv" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "printenv ~ (uutils) display value of environment VAR" diff --git a/src/uu/printf/Cargo.toml b/src/uu/printf/Cargo.toml index c37bcffc91e..cad30bd32b4 100644 --- a/src/uu/printf/Cargo.toml +++ b/src/uu/printf/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_printf" -version = "0.0.28" +version = "0.0.29" authors = ["Nathan Ross", "uutils developers"] license = "MIT" description = "printf ~ (uutils) FORMAT and display ARGUMENTS" diff --git a/src/uu/printf/src/printf.rs b/src/uu/printf/src/printf.rs index f86b7bd9fa2..f278affaede 100644 --- a/src/uu/printf/src/printf.rs +++ b/src/uu/printf/src/printf.rs @@ -2,9 +2,6 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. - -#![allow(dead_code)] - use clap::{crate_version, Arg, ArgAction, Command}; use std::io::stdout; use std::ops::ControlFlow; diff --git a/src/uu/ptx/Cargo.toml b/src/uu/ptx/Cargo.toml index de97790abcb..4d50a7cd419 100644 --- a/src/uu/ptx/Cargo.toml +++ b/src/uu/ptx/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_ptx" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "ptx ~ (uutils) display a permuted index of input" diff --git a/src/uu/pwd/Cargo.toml b/src/uu/pwd/Cargo.toml index e734c181907..c9290f16b70 100644 --- a/src/uu/pwd/Cargo.toml +++ b/src/uu/pwd/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_pwd" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "pwd ~ (uutils) display current working directory" diff --git a/src/uu/readlink/Cargo.toml b/src/uu/readlink/Cargo.toml index 4a226cad062..3792bb3de8d 100644 --- a/src/uu/readlink/Cargo.toml +++ b/src/uu/readlink/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_readlink" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "readlink ~ (uutils) display resolved path of PATHNAME" diff --git a/src/uu/realpath/Cargo.toml b/src/uu/realpath/Cargo.toml index dea52663e98..bd0154e2162 100644 --- a/src/uu/realpath/Cargo.toml +++ b/src/uu/realpath/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_realpath" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "realpath ~ (uutils) display resolved absolute path of PATHNAME" diff --git a/src/uu/rm/Cargo.toml b/src/uu/rm/Cargo.toml index 49e95bb06ce..c45dfe33d8a 100644 --- a/src/uu/rm/Cargo.toml +++ b/src/uu/rm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_rm" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "rm ~ (uutils) remove PATHNAME" diff --git a/src/uu/rm/src/rm.rs b/src/uu/rm/src/rm.rs index a89ba6db67f..f1f45cf5261 100644 --- a/src/uu/rm/src/rm.rs +++ b/src/uu/rm/src/rm.rs @@ -3,18 +3,22 @@ // 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; use std::ffi::{OsStr, OsString}; -use std::fs::{self, File, Metadata}; -use std::io::ErrorKind; +use std::fs::{self, Metadata}; 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 +294,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 +305,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 +332,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 +411,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!( @@ -505,27 +524,17 @@ fn prompt_file(path: &Path, options: &Options) -> bool { } } } - // File::open(path) doesn't open the file in write mode so we need to use file options to open it in also write mode to check if it can written too - match File::options().read(true).write(true).open(path) { - Ok(file) => { - let Ok(metadata) = file.metadata() else { - return true; - }; - if options.interactive == InteractiveMode::Always && !metadata.permissions().readonly() - { - return if metadata.len() == 0 { - prompt_yes!("remove regular empty file {}?", path.quote()) - } else { - prompt_yes!("remove file {}?", path.quote()) - }; - } - } - Err(err) => { - if err.kind() != ErrorKind::PermissionDenied { - return true; - } - } + let Ok(metadata) = fs::metadata(path) else { + return true; + }; + + if options.interactive == InteractiveMode::Always && !metadata.permissions().readonly() { + return if metadata.len() == 0 { + prompt_yes!("remove regular empty file {}?", path.quote()) + } else { + prompt_yes!("remove file {}?", path.quote()) + }; } prompt_file_permission_readonly(path) } @@ -559,6 +568,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 +609,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 +668,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/rmdir/Cargo.toml b/src/uu/rmdir/Cargo.toml index 3637b9130f7..286795e02c4 100644 --- a/src/uu/rmdir/Cargo.toml +++ b/src/uu/rmdir/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_rmdir" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "rmdir ~ (uutils) remove empty DIRECTORY" diff --git a/src/uu/runcon/Cargo.toml b/src/uu/runcon/Cargo.toml index 8bb10acbbbe..cda5fc2f776 100644 --- a/src/uu/runcon/Cargo.toml +++ b/src/uu/runcon/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_runcon" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "runcon ~ (uutils) run command with specified security context" diff --git a/src/uu/seq/Cargo.toml b/src/uu/seq/Cargo.toml index 99de71c1276..a063061f8fc 100644 --- a/src/uu/seq/Cargo.toml +++ b/src/uu/seq/Cargo.toml @@ -1,7 +1,7 @@ # spell-checker:ignore bigdecimal cfgs [package] name = "uu_seq" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "seq ~ (uutils) display a sequence of numbers" diff --git a/src/uu/seq/src/hexadecimalfloat.rs b/src/uu/seq/src/hexadecimalfloat.rs new file mode 100644 index 00000000000..e98074dd928 --- /dev/null +++ b/src/uu/seq/src/hexadecimalfloat.rs @@ -0,0 +1,404 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. +// spell-checker:ignore extendedbigdecimal bigdecimal hexdigit numberparse +use crate::extendedbigdecimal::ExtendedBigDecimal; +use crate::number::PreciseNumber; +use crate::numberparse::ParseNumberError; +use bigdecimal::BigDecimal; +use num_traits::FromPrimitive; + +/// The base of the hex number system +const HEX_RADIX: u32 = 16; + +/// Parse a number from a floating-point hexadecimal exponent notation. +/// +/// # Errors +/// Returns [`Err`] if: +/// - the input string is not a valid hexadecimal string +/// - the input data can't be interpreted as ['f64'] or ['BigDecimal'] +/// +/// # Examples +/// +/// ```rust,ignore +/// let input = "0x1.4p-2"; +/// let expected = 0.3125; +/// match input.parse_number::().unwrap().number { +/// ExtendedBigDecimal::BigDecimal(bd) => assert_eq!(bd.to_f64().unwrap(),expected), +/// _ => unreachable!() +/// }; +/// ``` +pub fn parse_number(s: &str) -> Result { + // Parse floating point parts + let (sign, remain) = parse_sign_multiplier(s.trim())?; + let remain = parse_hex_prefix(remain)?; + let (integral_part, remain) = parse_integral_part(remain)?; + let (fractional_part, remain) = parse_fractional_part(remain)?; + let (exponent_part, remain) = parse_exponent_part(remain)?; + + // Check parts. Rise error if: + // - The input string is not fully consumed + // - Only integral part is presented + // - Only exponent part is presented + // - All 3 parts are empty + match ( + integral_part, + fractional_part, + exponent_part, + remain.is_empty(), + ) { + (_, _, _, false) + | (Some(_), None, None, _) + | (None, None, Some(_), _) + | (None, None, None, _) => return Err(ParseNumberError::Float), + _ => (), + }; + + // Build a number from parts + let integral_value = integral_part.unwrap_or(0.0); + let fractional_value = fractional_part.unwrap_or(0.0); + let exponent_value = (2.0_f64).powi(exponent_part.unwrap_or(0)); + let value = sign * (integral_value + fractional_value) * exponent_value; + + // Build a PreciseNumber + let number = BigDecimal::from_f64(value).ok_or(ParseNumberError::Float)?; + let num_fractional_digits = number.fractional_digit_count().max(0) as u64; + let num_integral_digits = if value.abs() < 1.0 { + 0 + } else { + number.digits() - num_fractional_digits + }; + let num_integral_digits = num_integral_digits + if sign < 0.0 { 1 } else { 0 }; + + Ok(PreciseNumber::new( + ExtendedBigDecimal::BigDecimal(number), + num_integral_digits as usize, + num_fractional_digits as usize, + )) +} + +// Detect number precision similar to GNU coreutils. Refer to scan_arg in seq.c. There are still +// some differences from the GNU version, but this should be sufficient to test the idea. +pub fn parse_precision(s: &str) -> Option { + let hex_index = s.find(['x', 'X']); + let point_index = s.find('.'); + + if hex_index.is_some() { + // Hex value. Returns: + // - 0 for a hexadecimal integer (filled above) + // - None for a hexadecimal floating-point number (the default value of precision) + let power_index = s.find(['p', 'P']); + if point_index.is_none() && power_index.is_none() { + // No decimal point and no 'p' (power) => integer => precision = 0 + return Some(0); + } else { + return None; + } + } + + // This is a decimal floating point. The precision depends on two parameters: + // - the number of fractional digits + // - the exponent + // Let's detect the number of fractional digits + let fractional_length = if let Some(point_index) = point_index { + s[point_index + 1..] + .chars() + .take_while(|c| c.is_ascii_digit()) + .count() + } else { + 0 + }; + + let mut precision = Some(fractional_length); + + // Let's update the precision if exponent is present + if let Some(exponent_index) = s.find(['e', 'E']) { + let exponent_value: i32 = s[exponent_index + 1..].parse().unwrap_or(0); + if exponent_value < 0 { + precision = precision.map(|p| p + exponent_value.unsigned_abs() as usize); + } else { + precision = precision.map(|p| p - p.min(exponent_value as usize)); + } + } + precision +} + +/// Parse the sign multiplier. +/// +/// If a sign is present, the function reads and converts it into a multiplier. +/// If no sign is present, a multiplier of 1.0 is used. +/// +/// # Errors +/// +/// Returns [`Err`] if the input string does not start with a recognized sign or '0' symbol. +fn parse_sign_multiplier(s: &str) -> Result<(f64, &str), ParseNumberError> { + if let Some(remain) = s.strip_prefix('-') { + Ok((-1.0, remain)) + } else if let Some(remain) = s.strip_prefix('+') { + Ok((1.0, remain)) + } else if s.starts_with('0') { + Ok((1.0, s)) + } else { + Err(ParseNumberError::Float) + } +} + +/// Parses the `0x` prefix in a case-insensitive manner. +/// +/// # Errors +/// +/// Returns [`Err`] if the input string does not contain the required prefix. +fn parse_hex_prefix(s: &str) -> Result<&str, ParseNumberError> { + if !(s.starts_with("0x") || s.starts_with("0X")) { + return Err(ParseNumberError::Float); + } + Ok(&s[2..]) +} + +/// Parse the integral part in hexadecimal notation. +/// +/// The integral part is hexadecimal number located after the '0x' prefix and before '.' or 'p' +/// symbols. For example, the number 0x1.234p2 has an integral part 1. +/// +/// This part is optional. +/// +/// # Errors +/// +/// Returns [`Err`] if the integral part is present but a hexadecimal number cannot be parsed from the input string. +fn parse_integral_part(s: &str) -> Result<(Option, &str), ParseNumberError> { + // This part is optional. Skip parsing if symbol is not a hex digit. + let length = s.chars().take_while(|c| c.is_ascii_hexdigit()).count(); + if length > 0 { + let integer = + u64::from_str_radix(&s[..length], HEX_RADIX).map_err(|_| ParseNumberError::Float)?; + Ok((Some(integer as f64), &s[length..])) + } else { + Ok((None, s)) + } +} + +/// Parse the fractional part in hexadecimal notation. +/// +/// The function calculates the sum of the digits after the '.' (dot) sign. Each Nth digit is +/// interpreted as digit / 16^n, where n represents the position after the dot starting from 1. +/// +/// For example, the number 0x1.234p2 has a fractional part 234, which can be interpreted as +/// 2/16^1 + 3/16^2 + 4/16^3, where 16 is the radix of the hexadecimal number system. This equals +/// 0.125 + 0.01171875 + 0.0009765625 = 0.1376953125 in decimal. And this is exactly what the +/// function does. +/// +/// This part is optional. +/// +/// # Errors +/// +/// Returns [`Err`] if the fractional part is present but a hexadecimal number cannot be parsed from the input string. +fn parse_fractional_part(s: &str) -> Result<(Option, &str), ParseNumberError> { + // This part is optional and follows after the '.' symbol. Skip parsing if the dot is not present. + if !s.starts_with('.') { + return Ok((None, s)); + } + + let s = &s[1..]; + let mut multiplier = 1.0 / HEX_RADIX as f64; + let mut total = 0.0; + let mut length = 0; + + for c in s.chars().take_while(|c| c.is_ascii_hexdigit()) { + let digit = c + .to_digit(HEX_RADIX) + .map(|x| x as u8) + .ok_or(ParseNumberError::Float)?; + total += (digit as f64) * multiplier; + multiplier /= HEX_RADIX as f64; + length += 1; + } + + if length == 0 { + return Err(ParseNumberError::Float); + } + Ok((Some(total), &s[length..])) +} + +/// Parse the exponent part in hexadecimal notation. +/// +/// The exponent part is a decimal number located after the 'p' symbol. +/// For example, the number 0x1.234p2 has an exponent part 2. +/// +/// This part is optional. +/// +/// # Errors +/// +/// Returns [`Err`] if the exponent part is presented but a decimal number cannot be parsed from +/// the input string. +fn parse_exponent_part(s: &str) -> Result<(Option, &str), ParseNumberError> { + // This part is optional and follows after 'p' or 'P' symbols. Skip parsing if the symbols are not present + if !(s.starts_with('p') || s.starts_with('P')) { + return Ok((None, s)); + } + + let s = &s[1..]; + let length = s + .chars() + .take_while(|c| c.is_ascii_digit() || *c == '-' || *c == '+') + .count(); + + if length == 0 { + return Err(ParseNumberError::Float); + } + + let value = s[..length].parse().map_err(|_| ParseNumberError::Float)?; + Ok((Some(value), &s[length..])) +} + +#[cfg(test)] +mod tests { + + use super::{parse_number, parse_precision}; + use crate::{numberparse::ParseNumberError, ExtendedBigDecimal}; + use bigdecimal::BigDecimal; + use num_traits::ToPrimitive; + + fn parse_big_decimal(s: &str) -> Result { + match parse_number(s)?.number { + ExtendedBigDecimal::BigDecimal(bd) => Ok(bd), + _ => Err(ParseNumberError::Float), + } + } + + fn parse_f64(s: &str) -> Result { + parse_big_decimal(s)? + .to_f64() + .ok_or(ParseNumberError::Float) + } + + #[test] + fn test_parse_precise_number_case_insensitive() { + assert_eq!(parse_f64("0x1P1").unwrap(), 2.0); + assert_eq!(parse_f64("0x1p1").unwrap(), 2.0); + } + + #[test] + fn test_parse_precise_number_plus_minus_prefixes() { + assert_eq!(parse_f64("+0x1p1").unwrap(), 2.0); + assert_eq!(parse_f64("-0x1p1").unwrap(), -2.0); + } + + #[test] + fn test_parse_precise_number_power_signs() { + assert_eq!(parse_f64("0x1p1").unwrap(), 2.0); + assert_eq!(parse_f64("0x1p+1").unwrap(), 2.0); + assert_eq!(parse_f64("0x1p-1").unwrap(), 0.5); + } + + #[test] + fn test_parse_precise_number_hex() { + assert_eq!(parse_f64("0xd.dp-1").unwrap(), 6.90625); + } + + #[test] + fn test_parse_precise_number_no_power() { + assert_eq!(parse_f64("0x123.a").unwrap(), 291.625); + } + + #[test] + fn test_parse_precise_number_no_fractional() { + assert_eq!(parse_f64("0x333p-4").unwrap(), 51.1875); + } + + #[test] + fn test_parse_precise_number_no_integral() { + assert_eq!(parse_f64("0x.9").unwrap(), 0.5625); + assert_eq!(parse_f64("0x.9p2").unwrap(), 2.25); + } + + #[test] + fn test_parse_precise_number_from_valid_values() { + assert_eq!(parse_f64("0x1p1").unwrap(), 2.0); + assert_eq!(parse_f64("+0x1p1").unwrap(), 2.0); + assert_eq!(parse_f64("-0x1p1").unwrap(), -2.0); + assert_eq!(parse_f64("0x1p-1").unwrap(), 0.5); + assert_eq!(parse_f64("0x1.8").unwrap(), 1.5); + assert_eq!(parse_f64("-0x1.8").unwrap(), -1.5); + assert_eq!(parse_f64("0x1.8p2").unwrap(), 6.0); + assert_eq!(parse_f64("0x1.8p+2").unwrap(), 6.0); + assert_eq!(parse_f64("0x1.8p-2").unwrap(), 0.375); + assert_eq!(parse_f64("0x.8").unwrap(), 0.5); + assert_eq!(parse_f64("0x10p0").unwrap(), 16.0); + assert_eq!(parse_f64("0x0.0").unwrap(), 0.0); + assert_eq!(parse_f64("0x0p0").unwrap(), 0.0); + assert_eq!(parse_f64("0x0.0p0").unwrap(), 0.0); + assert_eq!(parse_f64("-0x.1p-3").unwrap(), -0.0078125); + assert_eq!(parse_f64("-0x.ep-3").unwrap(), -0.109375); + } + + #[test] + fn test_parse_float_from_invalid_values() { + let expected_error = ParseNumberError::Float; + assert_eq!(parse_f64("").unwrap_err(), expected_error); + assert_eq!(parse_f64("1").unwrap_err(), expected_error); + assert_eq!(parse_f64("1p").unwrap_err(), expected_error); + assert_eq!(parse_f64("0x").unwrap_err(), expected_error); + assert_eq!(parse_f64("0xG").unwrap_err(), expected_error); + assert_eq!(parse_f64("0xp").unwrap_err(), expected_error); + assert_eq!(parse_f64("0xp3").unwrap_err(), expected_error); + assert_eq!(parse_f64("0x1").unwrap_err(), expected_error); + assert_eq!(parse_f64("0x1.").unwrap_err(), expected_error); + assert_eq!(parse_f64("0x1p").unwrap_err(), expected_error); + assert_eq!(parse_f64("0x1p+").unwrap_err(), expected_error); + assert_eq!(parse_f64("-0xx1p1").unwrap_err(), expected_error); + assert_eq!(parse_f64("0x1.k").unwrap_err(), expected_error); + assert_eq!(parse_f64("0x1").unwrap_err(), expected_error); + assert_eq!(parse_f64("-0x1pa").unwrap_err(), expected_error); + assert_eq!(parse_f64("0x1.1pk").unwrap_err(), expected_error); + assert_eq!(parse_f64("0x1.8p2z").unwrap_err(), expected_error); + assert_eq!(parse_f64("0x1p3.2").unwrap_err(), expected_error); + assert_eq!(parse_f64("-0x.ep-3z").unwrap_err(), expected_error); + } + + #[test] + fn test_parse_precise_number_count_digits() { + let precise_num = parse_number("0x1.2").unwrap(); // 1.125 decimal + assert_eq!(precise_num.num_integral_digits, 1); + assert_eq!(precise_num.num_fractional_digits, 3); + + let precise_num = parse_number("-0x1.2").unwrap(); // -1.125 decimal + assert_eq!(precise_num.num_integral_digits, 2); + assert_eq!(precise_num.num_fractional_digits, 3); + + let precise_num = parse_number("0x123.8").unwrap(); // 291.5 decimal + assert_eq!(precise_num.num_integral_digits, 3); + assert_eq!(precise_num.num_fractional_digits, 1); + + let precise_num = parse_number("-0x123.8").unwrap(); // -291.5 decimal + assert_eq!(precise_num.num_integral_digits, 4); + assert_eq!(precise_num.num_fractional_digits, 1); + } + + #[test] + fn test_parse_precision_valid_values() { + assert_eq!(parse_precision("1"), Some(0)); + assert_eq!(parse_precision("0x1"), Some(0)); + assert_eq!(parse_precision("0x1.1"), None); + assert_eq!(parse_precision("0x1.1p2"), None); + assert_eq!(parse_precision("0x1.1p-2"), None); + assert_eq!(parse_precision(".1"), Some(1)); + assert_eq!(parse_precision("1.1"), Some(1)); + assert_eq!(parse_precision("1.12"), Some(2)); + assert_eq!(parse_precision("1.12345678"), Some(8)); + assert_eq!(parse_precision("1.12345678e-3"), Some(11)); + assert_eq!(parse_precision("1.1e-1"), Some(2)); + assert_eq!(parse_precision("1.1e-3"), Some(4)); + } + + #[test] + fn test_parse_precision_invalid_values() { + // Just to make sure it doesn't crash on incomplete values/bad format + // Good enough for now. + assert_eq!(parse_precision("1."), Some(0)); + assert_eq!(parse_precision("1e"), Some(0)); + assert_eq!(parse_precision("1e-"), Some(0)); + assert_eq!(parse_precision("1e+"), Some(0)); + assert_eq!(parse_precision("1em"), Some(0)); + } +} diff --git a/src/uu/seq/src/number.rs b/src/uu/seq/src/number.rs index 314c842ba15..ec6ac0f1687 100644 --- a/src/uu/seq/src/number.rs +++ b/src/uu/seq/src/number.rs @@ -19,6 +19,8 @@ use crate::extendedbigdecimal::ExtendedBigDecimal; pub struct PreciseNumber { pub number: ExtendedBigDecimal, pub num_integral_digits: usize, + + #[allow(dead_code)] pub num_fractional_digits: usize, } diff --git a/src/uu/seq/src/numberparse.rs b/src/uu/seq/src/numberparse.rs index 5a5c64bb991..d00db16fa13 100644 --- a/src/uu/seq/src/numberparse.rs +++ b/src/uu/seq/src/numberparse.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 extendedbigdecimal bigdecimal numberparse +// spell-checker:ignore extendedbigdecimal bigdecimal numberparse hexadecimalfloat //! Parsing numbers for use in `seq`. //! //! This module provides an implementation of [`FromStr`] for the @@ -16,6 +16,7 @@ use num_traits::Num; use num_traits::Zero; use crate::extendedbigdecimal::ExtendedBigDecimal; +use crate::hexadecimalfloat; use crate::number::PreciseNumber; /// An error returned when parsing a number fails. @@ -102,20 +103,36 @@ fn parse_exponent_no_decimal(s: &str, j: usize) -> Result |i64::MAX| (i.e. |−2^63| > |2^63−1|) inverting a + // valid negative value could result in an overflow. To prevent this, we + // limit the minimal value with i64::MIN + 1. + let exponent = exponent.max(i64::MIN + 1); + let base: BigInt = s[..j].parse().map_err(|_| ParseNumberError::Float)?; + let x = if base.is_zero() { + BigDecimal::zero() + } else { + BigDecimal::from_bigint(base, -exponent) + }; 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 +217,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 +245,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)? } }; @@ -267,6 +300,14 @@ fn parse_decimal_and_exponent( /// assert_eq!(actual, expected); /// ``` fn parse_hexadecimal(s: &str) -> Result { + if s.find(['.', 'p', 'P']).is_some() { + hexadecimalfloat::parse_number(s) + } else { + parse_hexadecimal_integer(s) + } +} + +fn parse_hexadecimal_integer(s: &str) -> Result { let (is_neg, s) = if s.starts_with('-') { (true, &s[3..]) } else { @@ -312,7 +353,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 +363,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 +422,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()) @@ -560,4 +602,18 @@ mod tests { assert_eq!(num_fractional_digits("-0e-1"), 1); assert_eq!(num_fractional_digits("-0.0e-1"), 2); } + + #[test] + fn test_parse_min_exponents() { + // Make sure exponents <= i64::MIN do not cause errors + assert!("1e-9223372036854775807".parse::().is_ok()); + assert!("1e-9223372036854775808".parse::().is_ok()); + } + + #[test] + fn test_parse_max_exponents() { + // Make sure exponents >= i64::MAX cause errors + assert!("1e9223372036854775807".parse::().is_err()); + assert!("1e9223372036854775808".parse::().is_err()); + } } diff --git a/src/uu/seq/src/seq.rs b/src/uu/seq/src/seq.rs index 96ae83ba0a6..0ee5101d7ef 100644 --- a/src/uu/seq/src/seq.rs +++ b/src/uu/seq/src/seq.rs @@ -2,18 +2,21 @@ // // 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 +// spell-checker:ignore (ToDO) bigdecimal extendedbigdecimal numberparse hexadecimalfloat +use std::ffi::OsString; use std::io::{stdout, ErrorKind, Write}; use clap::{crate_version, Arg, ArgAction, Command}; use num_traits::{ToPrimitive, Zero}; use uucore::error::{FromIo, UResult}; -use uucore::format::{num_format, Format}; +use uucore::format::{num_format, sprintf, Format, FormatArgument}; use uucore::{format_usage, help_about, help_usage}; mod error; mod extendedbigdecimal; +mod hexadecimalfloat; + // public to allow fuzzing #[cfg(fuzzing)] pub mod number; @@ -47,9 +50,45 @@ 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() +} + +fn select_precision( + first: Option, + increment: Option, + last: Option, +) -> Option { + match (first, increment, last) { + (Some(0), Some(0), Some(0)) => Some(0), + (Some(f), Some(i), Some(_)) => Some(f.max(i)), + _ => None, + } +} + #[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); @@ -74,32 +113,32 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { format: matches.get_one::(OPT_FORMAT).map(|s| s.as_str()), }; - let first = if numbers.len() > 1 { + let (first, first_precision) = if numbers.len() > 1 { match numbers[0].parse() { - Ok(num) => num, + Ok(num) => (num, hexadecimalfloat::parse_precision(numbers[0])), Err(e) => return Err(SeqError::ParseError(numbers[0].to_string(), e).into()), } } else { - PreciseNumber::one() + (PreciseNumber::one(), Some(0)) }; - let increment = if numbers.len() > 2 { + let (increment, increment_precision) = if numbers.len() > 2 { match numbers[1].parse() { - Ok(num) => num, + Ok(num) => (num, hexadecimalfloat::parse_precision(numbers[1])), Err(e) => return Err(SeqError::ParseError(numbers[1].to_string(), e).into()), } } else { - PreciseNumber::one() + (PreciseNumber::one(), Some(0)) }; if increment.is_zero() { return Err(SeqError::ZeroIncrement(numbers[1].to_string()).into()); } - let last: PreciseNumber = { + let (last, last_precision): (PreciseNumber, Option) = { // We are guaranteed that `numbers.len()` is greater than zero // and at most three because of the argument specification in // `uu_app()`. let n: usize = numbers.len(); match numbers[n - 1].parse() { - Ok(num) => num, + Ok(num) => (num, hexadecimalfloat::parse_precision(numbers[n - 1])), Err(e) => return Err(SeqError::ParseError(numbers[n - 1].to_string(), e).into()), } }; @@ -108,9 +147,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { .num_integral_digits .max(increment.num_integral_digits) .max(last.num_integral_digits); - let largest_dec = first - .num_fractional_digits - .max(increment.num_fractional_digits); + + let precision = select_precision(first_precision, increment_precision, last_precision); let format = match options.format { Some(f) => { @@ -121,7 +159,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { }; let result = print_seq( (first.number, increment.number, last.number), - largest_dec, + precision, &options.separator, &options.terminator, options.equal_width, @@ -138,7 +176,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 +206,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), ) @@ -183,26 +223,42 @@ fn done_printing(next: &T, increment: &T, last: &T) -> boo } } +fn format_bigdecimal(value: &bigdecimal::BigDecimal) -> Option { + let format_arguments = &[FormatArgument::Float(value.to_f64()?)]; + let value_as_bytes = sprintf("%g", format_arguments).ok()?; + String::from_utf8(value_as_bytes).ok() +} + /// Write a big decimal formatted according to the given parameters. fn write_value_float( writer: &mut impl Write, value: &ExtendedBigDecimal, width: usize, - precision: usize, + precision: Option, ) -> std::io::Result<()> { - let value_as_str = - if *value == ExtendedBigDecimal::Infinity || *value == ExtendedBigDecimal::MinusInfinity { - format!("{value:>width$.precision$}") - } else { - format!("{value:>0width$.precision$}") - }; + let value_as_str = match precision { + // format with precision: decimal floats and integers + Some(precision) => match value { + ExtendedBigDecimal::Infinity | ExtendedBigDecimal::MinusInfinity => { + format!("{value:>width$.precision$}") + } + _ => format!("{value:>0width$.precision$}"), + }, + // format without precision: hexadecimal floats + None => match value { + ExtendedBigDecimal::BigDecimal(bd) => { + format_bigdecimal(bd).unwrap_or_else(|| "{value}".to_owned()) + } + _ => format!("{value:>0width$}"), + }, + }; write!(writer, "{value_as_str}") } /// Floating point based code path fn print_seq( range: RangeFloat, - largest_dec: usize, + precision: Option, separator: &str, terminator: &str, pad: bool, @@ -214,7 +270,13 @@ fn print_seq( let (first, increment, last) = range; let mut value = first; let padding = if pad { - padding + if largest_dec > 0 { largest_dec + 1 } else { 0 } + let precision_value = precision.unwrap_or(0); + padding + + if precision_value > 0 { + precision_value + 1 + } else { + 0 + } } else { 0 }; @@ -246,7 +308,7 @@ fn print_seq( }; f.fmt(&mut stdout, float)?; } - None => write_value_float(&mut stdout, &value, padding, largest_dec)?, + None => write_value_float(&mut stdout, &value, padding, precision)?, } // TODO Implement augmenting addition. value = value + increment.clone(); diff --git a/src/uu/shred/Cargo.toml b/src/uu/shred/Cargo.toml index 0f5f1d6aab8..10394565a37 100644 --- a/src/uu/shred/Cargo.toml +++ b/src/uu/shred/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_shred" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "shred ~ (uutils) hide former FILE contents with repeated overwrites" diff --git a/src/uu/shuf/Cargo.toml b/src/uu/shuf/Cargo.toml index 8b7ae28f8ea..f8b887b7e87 100644 --- a/src/uu/shuf/Cargo.toml +++ b/src/uu/shuf/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_shuf" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "shuf ~ (uutils) display random permutations of input lines" diff --git a/src/uu/shuf/src/shuf.rs b/src/uu/shuf/src/shuf.rs index 260b5130c53..2d8023448a0 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/sleep/Cargo.toml b/src/uu/sleep/Cargo.toml index 8e72dd82a21..bb6e2e13103 100644 --- a/src/uu/sleep/Cargo.toml +++ b/src/uu/sleep/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_sleep" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "sleep ~ (uutils) pause for DURATION" diff --git a/src/uu/sort/Cargo.toml b/src/uu/sort/Cargo.toml index 799b4831d9d..99c1254c0cf 100644 --- a/src/uu/sort/Cargo.toml +++ b/src/uu/sort/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_sort" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "sort ~ (uutils) sort input lines" diff --git a/src/uu/sort/src/ext_sort.rs b/src/uu/sort/src/ext_sort.rs index 18309881229..57e434e99b2 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 c0457ffa4dc..300733d1e36 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 c2e752bdf6a..8b6fcbb2514 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/Cargo.toml b/src/uu/split/Cargo.toml index cc69a6bc4cb..8e09eb76d6b 100644 --- a/src/uu/split/Cargo.toml +++ b/src/uu/split/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_split" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "split ~ (uutils) split input into output files" diff --git a/src/uu/split/src/filenames.rs b/src/uu/split/src/filenames.rs index d2ce1beb316..9e899a417a9 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 11fa04184df..053d86e8c28 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 @@ -919,204 +919,6 @@ impl<'a> Write for LineChunkWriter<'a> { } } -/// Write lines to each sequential output files, limited by bytes. -/// -/// This struct maintains an underlying writer representing the -/// current chunk of the output. On each call to [`write`], it writes -/// as many lines as possible to the current chunk without exceeding -/// the specified byte limit. If a single line has more bytes than the -/// limit, then fill an entire single chunk with those bytes and -/// handle the remainder of the line as if it were its own distinct -/// line. As many new underlying writers are created as needed to -/// write all the data in the input buffer. -struct LineBytesChunkWriter<'a> { - /// Parameters for creating the underlying writer for each new chunk. - settings: &'a Settings, - - /// The maximum number of bytes allowed for a single chunk of output. - chunk_size: u64, - - /// Running total of number of chunks that have been completed. - num_chunks_written: usize, - - /// Remaining capacity in number of bytes in the current chunk. - /// - /// This number starts at `chunk_size` and decreases as lines are - /// written. Once it reaches zero, a writer for a new chunk is - /// initialized and this number gets reset to `chunk_size`. - num_bytes_remaining_in_current_chunk: usize, - - /// The underlying writer for the current chunk. - /// - /// Once the number of bytes written to this writer exceeds - /// `chunk_size`, a new writer is initialized and assigned to this - /// field. - inner: BufWriter>, - - /// Iterator that yields filenames for each chunk. - filename_iterator: FilenameIterator<'a>, -} - -impl<'a> LineBytesChunkWriter<'a> { - fn new(chunk_size: u64, settings: &'a Settings) -> UResult { - let mut filename_iterator = FilenameIterator::new(&settings.prefix, &settings.suffix)?; - let filename = filename_iterator - .next() - .ok_or_else(|| USimpleError::new(1, "output file suffixes exhausted"))?; - if settings.verbose { - println!("creating file {}", filename.quote()); - } - let inner = settings.instantiate_current_writer(&filename, true)?; - Ok(LineBytesChunkWriter { - settings, - chunk_size, - num_bytes_remaining_in_current_chunk: usize::try_from(chunk_size).unwrap(), - num_chunks_written: 0, - inner, - filename_iterator, - }) - } -} - -impl<'a> Write for LineBytesChunkWriter<'a> { - /// 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 - /// bytes and handle the remainder of the line as if it were - /// its own distinct line. - /// - /// For example: if the `chunk_size` is 8 and the input is: - /// - /// ```text - /// aaaaaaaaa\nbbbb\ncccc\ndd\nee\n - /// ``` - /// - /// then the output gets broken into chunks like this: - /// - /// ```text - /// chunk 0 chunk 1 chunk 2 chunk 3 - /// - /// 0 1 2 - /// 01234567 89 01234 56789 012 345 6 - /// |------| |-------| |--------| |---| - /// aaaaaaaa a\nbbbb\n cccc\ndd\n ee\n - /// ``` - /// - /// Implements `--line-bytes=SIZE` - fn write(&mut self, mut buf: &[u8]) -> std::io::Result { - // The total number of bytes written during the loop below. - // - // It is necessary to keep this running total because we may - // be making multiple calls to `write()` on multiple different - // underlying writers and we want the final reported number of - // bytes written to reflect the total number of bytes written - // to all of the underlying writers. - let mut total_bytes_written = 0; - - // Loop until we have written all bytes in the input buffer - // (or an IO error occurs). - loop { - // If the buffer is empty, then we are done writing. - if buf.is_empty() { - return Ok(total_bytes_written); - } - - // If we have filled the current chunk with bytes, then - // start a new chunk and initialize its corresponding - // writer. - if self.num_bytes_remaining_in_current_chunk == 0 { - self.num_chunks_written += 1; - let filename = self.filename_iterator.next().ok_or_else(|| { - std::io::Error::new(ErrorKind::Other, "output file suffixes exhausted") - })?; - if self.settings.verbose { - println!("creating file {}", filename.quote()); - } - self.inner = self.settings.instantiate_current_writer(&filename, true)?; - self.num_bytes_remaining_in_current_chunk = self.chunk_size.try_into().unwrap(); - } - - // Find the first separator (default - newline character) in the buffer. - let sep = self.settings.separator; - match memchr::memchr(sep, buf) { - // If there is no separator character and the buffer is - // not empty, then write as many bytes as we can and - // then move on to the next chunk if necessary. - None => { - let end = self.num_bytes_remaining_in_current_chunk; - - // This is ugly but here to match GNU behavior. If the input - // doesn't end with a separator, pretend that it does for handling - // the second to last segment chunk. See `line-bytes.sh`. - if end == buf.len() - && self.num_bytes_remaining_in_current_chunk - < self.chunk_size.try_into().unwrap() - && buf[buf.len() - 1] != sep - { - self.num_bytes_remaining_in_current_chunk = 0; - } else { - let num_bytes_written = custom_write( - &buf[..end.min(buf.len())], - &mut self.inner, - self.settings, - )?; - self.num_bytes_remaining_in_current_chunk -= num_bytes_written; - total_bytes_written += num_bytes_written; - buf = &buf[num_bytes_written..]; - } - } - - // If there is a separator character and the line - // (including the separator character) will fit in the - // current chunk, then write the entire line and - // continue to the next iteration. (See chunk 1 in the - // example comment above.) - Some(i) if i < self.num_bytes_remaining_in_current_chunk => { - let num_bytes_written = - custom_write(&buf[..=i], &mut self.inner, self.settings)?; - self.num_bytes_remaining_in_current_chunk -= num_bytes_written; - total_bytes_written += num_bytes_written; - buf = &buf[num_bytes_written..]; - } - - // If there is a separator character, the line - // (including the separator character) will not fit in - // the current chunk, *and* no other lines have been - // written to the current chunk, then write as many - // bytes as we can and continue to the next - // iteration. (See chunk 0 in the example comment - // above.) - Some(_) - if self.num_bytes_remaining_in_current_chunk - == self.chunk_size.try_into().unwrap() => - { - let end = self.num_bytes_remaining_in_current_chunk; - let num_bytes_written = - custom_write(&buf[..end], &mut self.inner, self.settings)?; - self.num_bytes_remaining_in_current_chunk -= num_bytes_written; - total_bytes_written += num_bytes_written; - buf = &buf[num_bytes_written..]; - } - - // If there is a separator character, the line - // (including the separator character) will not fit in - // the current chunk, and at least one other line has - // been written to the current chunk, then signal to - // the next iteration that a new chunk needs to be - // created and continue to the next iteration of the - // loop to try writing the line there. - Some(_) => { - self.num_bytes_remaining_in_current_chunk = 0; - } - } - } - } - - fn flush(&mut self) -> std::io::Result<()> { - self.inner.flush() - } -} - /// Output file parameters struct OutFile { filename: String, @@ -1629,6 +1431,114 @@ where Ok(()) } +/// Like `std::io::Lines`, but includes the line ending character. +/// +/// This struct is generally created by calling `lines_with_sep` on a +/// reader. +pub struct LinesWithSep { + inner: R, + separator: u8, +} + +impl Iterator for LinesWithSep +where + R: BufRead, +{ + type Item = std::io::Result>; + + /// Read bytes from a buffer up to the requested number of lines. + fn next(&mut self) -> Option { + let mut buf = vec![]; + match self.inner.read_until(self.separator, &mut buf) { + Ok(0) => None, + Ok(_) => Some(Ok(buf)), + Err(e) => Some(Err(e)), + } + } +} + +/// Like `std::str::lines` but includes the line ending character. +/// +/// The `separator` defines the character to interpret as the line +/// ending. For the usual notion of "line", set this to `b'\n'`. +pub fn lines_with_sep(reader: R, separator: u8) -> LinesWithSep +where + R: BufRead, +{ + LinesWithSep { + inner: reader, + separator, + } +} + +fn line_bytes(settings: &Settings, reader: &mut R, chunk_size: usize) -> UResult<()> +where + R: BufRead, +{ + let mut filename_iterator = FilenameIterator::new(&settings.prefix, &settings.suffix)?; + + // Initialize the writer just to satisfy the compiler. It is going + // to be overwritten for sure at the beginning of the loop below + // because we start with `remaining == 0`, indicating that a new + // chunk should start. + let mut writer: BufWriter> = + BufWriter::new(Box::new(std::io::Cursor::new(vec![]))); + + let mut remaining = 0; + for line in lines_with_sep(reader, settings.separator) { + let line = line?; + let mut line = &line[..]; + loop { + if remaining == 0 { + let filename = filename_iterator + .next() + .ok_or_else(|| USimpleError::new(1, "output file suffixes exhausted"))?; + if settings.verbose { + println!("creating file {}", filename.quote()); + } + writer = settings.instantiate_current_writer(&filename, true)?; + remaining = chunk_size; + } + + // Special case: if this is the last line and it doesn't end + // with a newline character, then count its length as though + // it did end with a newline. If that puts it over the edge + // of this chunk, continue to the next chunk. + if line.len() == remaining + && remaining < chunk_size + && line[line.len() - 1] != settings.separator + { + remaining = 0; + continue; + } + + // If the entire line fits in this chunk, write it and + // continue to the next line. + if line.len() <= remaining { + custom_write_all(line, &mut writer, settings)?; + remaining -= line.len(); + break; + } + + // If the line is too large to fit in *any* chunk and we are + // at the start of a new chunk, write as much as we can of + // it and pass the remainder along to the next chunk. + if line.len() > chunk_size && remaining == chunk_size { + custom_write_all(&line[..chunk_size], &mut writer, settings)?; + line = &line[chunk_size..]; + remaining = 0; + continue; + } + + // If the line is too large to fit in *this* chunk, but + // might otherwise fit in the next chunk, then just continue + // to the next chunk and let it be handled there. + remaining = 0; + } + } + Ok(()) +} + #[allow(clippy::cognitive_complexity)] fn split(settings: &Settings) -> UResult<()> { let r_box = if settings.input == "-" { @@ -1701,23 +1611,6 @@ fn split(settings: &Settings) -> UResult<()> { }, } } - Strategy::LineBytes(chunk_size) => { - let mut writer = LineBytesChunkWriter::new(chunk_size, settings)?; - match std::io::copy(&mut reader, &mut writer) { - Ok(_) => Ok(()), - Err(e) => match e.kind() { - // TODO Since the writer object controls the creation of - // new files, we need to rely on the `std::io::Result` - // returned by its `write()` method to communicate any - // errors to this calling scope. If a new file cannot be - // created because we have exceeded the number of - // allowable filenames, we use `ErrorKind::Other` to - // indicate that. A special error message needs to be - // printed in that case. - ErrorKind::Other => Err(USimpleError::new(1, format!("{e}"))), - _ => Err(uio_error!(e, "input/output error")), - }, - } - } + Strategy::LineBytes(chunk_size) => line_bytes(settings, &mut reader, chunk_size as usize), } } diff --git a/src/uu/stat/Cargo.toml b/src/uu/stat/Cargo.toml index 191183cfa41..c503426d142 100644 --- a/src/uu/stat/Cargo.toml +++ b/src/uu/stat/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_stat" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "stat ~ (uutils) display FILE status" diff --git a/src/uu/stat/src/stat.rs b/src/uu/stat/src/stat.rs index ee417834461..a6220267314 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"); @@ -90,16 +94,48 @@ pub enum OutputType { Unsigned(u64), UnsignedHex(u64), UnsignedOct(u32), + Float(f64), 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, Clone, Copy)] +enum Precision { + NotSpecified, + NoNumber, + Number(usize), +} + #[derive(Debug, PartialEq, Eq)] enum Token { Char(char), + Byte(u8), Directive { flag: Flags, width: usize, - precision: Option, + precision: Precision, format: char, }, } @@ -210,10 +246,10 @@ struct Stater { /// * `output` - A reference to the OutputType enum containing the value to be printed. /// * `flags` - A Flags struct containing formatting flags. /// * `width` - The width of the field for the printed output. -/// * `precision` - An Option containing the precision value. +/// * `precision` - How many digits of precision, if any. /// /// This function delegates the printing process to more specialized functions depending on the output type. -fn print_it(output: &OutputType, flags: Flags, width: usize, precision: Option) { +fn print_it(output: &OutputType, flags: Flags, width: usize, precision: Precision) { // If the precision is given as just '.', the precision is taken to be zero. // A negative precision is taken as if the precision were omitted. // This gives the minimum number of digits to appear for d, i, o, u, x, and X conversions, @@ -243,7 +279,7 @@ fn print_it(output: &OutputType, flags: Flags, width: usize, precision: Option print_str(s, &flags, width, precision), @@ -255,6 +291,9 @@ fn print_it(output: &OutputType, flags: Flags, width: usize, precision: Option { print_unsigned_hex(*num, &flags, width, precision, padding_char); } + OutputType::Float(num) => { + print_float(*num, &flags, width, precision, padding_char); + } OutputType::Unknown => print!("?"), } } @@ -264,13 +303,12 @@ fn print_it(output: &OutputType, flags: Flags, width: usize, precision: Option) -> Padding { - if flags.zero && !flags.left && precision.is_none() { +fn determine_padding_char(flags: &Flags) -> Padding { + if flags.zero && !flags.left { Padding::Zero } else { Padding::Space @@ -284,15 +322,102 @@ fn determine_padding_char(flags: &Flags, precision: &Option) -> Padding { /// * `s` - The string to be printed. /// * `flags` - A reference to the Flags struct containing formatting flags. /// * `width` - The width of the field for the printed string. -/// * `precision` - An Option containing the precision value. -fn print_str(s: &str, flags: &Flags, width: usize, precision: Option) { +/// * `precision` - How many digits of precision, if any. +fn print_str(s: &str, flags: &Flags, width: usize, precision: Precision) { let s = match precision { - Some(p) if p < s.len() => &s[..p], + Precision::Number(p) if p < s.len() => &s[..p], _ => s, }; 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 @@ -300,13 +425,13 @@ fn print_str(s: &str, flags: &Flags, width: usize, precision: Option) { /// * `num` - The integer value to be printed. /// * `flags` - A reference to the Flags struct containing formatting flags. /// * `width` - The width of the field for the printed integer. -/// * `precision` - An Option containing the precision value. +/// * `precision` - How many digits of precision, if any. /// * `padding_char` - The padding character as determined by `determine_padding_char`. fn print_integer( num: i64, flags: &Flags, width: usize, - precision: Option, + precision: Precision, padding_char: Padding, ) { let num = num.to_string(); @@ -322,13 +447,66 @@ fn print_integer( } else { "" }; - let extended = format!( - "{prefix}{arg:0>precision$}", - precision = precision.unwrap_or(0) - ); + let extended = match precision { + Precision::NotSpecified => format!("{prefix}{arg}"), + Precision::NoNumber => format!("{prefix}{arg}"), + Precision::Number(p) => format!("{prefix}{arg:0>precision$}", precision = p), + }; pad_and_print(&extended, flags.left, width, padding_char); } +/// Truncate a float to the given number of digits after the decimal point. +fn precision_trunc(num: f64, precision: Precision) -> String { + // GNU `stat` doesn't round, it just seems to truncate to the + // given precision: + // + // $ stat -c "%.5Y" /dev/pts/ptmx + // 1736344012.76399 + // $ stat -c "%.4Y" /dev/pts/ptmx + // 1736344012.7639 + // $ stat -c "%.3Y" /dev/pts/ptmx + // 1736344012.763 + // + // Contrast this with `printf`, which seems to round the + // numbers: + // + // $ printf "%.5f\n" 1736344012.76399 + // 1736344012.76399 + // $ printf "%.4f\n" 1736344012.76399 + // 1736344012.7640 + // $ printf "%.3f\n" 1736344012.76399 + // 1736344012.764 + // + let num_str = num.to_string(); + let n = num_str.len(); + match (num_str.find('.'), precision) { + (None, Precision::NotSpecified) => num_str, + (None, Precision::NoNumber) => num_str, + (None, Precision::Number(0)) => num_str, + (None, Precision::Number(p)) => format!("{num_str}.{zeros}", zeros = "0".repeat(p)), + (Some(i), Precision::NotSpecified) => num_str[..i].to_string(), + (Some(_), Precision::NoNumber) => num_str, + (Some(i), Precision::Number(0)) => num_str[..i].to_string(), + (Some(i), Precision::Number(p)) if p < n - i => num_str[..i + 1 + p].to_string(), + (Some(i), Precision::Number(p)) => { + format!("{num_str}{zeros}", zeros = "0".repeat(p - (n - i - 1))) + } + } +} + +fn print_float(num: f64, flags: &Flags, width: usize, precision: Precision, padding_char: Padding) { + let prefix = if flags.sign { + "+" + } else if flags.space { + " " + } else { + "" + }; + let num_str = precision_trunc(num, precision); + let extended = format!("{prefix}{num_str}"); + pad_and_print(&extended, flags.left, width, padding_char) +} + /// Prints an unsigned integer value based on the provided flags, width, and precision. /// /// # Arguments @@ -336,13 +514,13 @@ fn print_integer( /// * `num` - The unsigned integer value to be printed. /// * `flags` - A reference to the Flags struct containing formatting flags. /// * `width` - The width of the field for the printed unsigned integer. -/// * `precision` - An Option containing the precision value. +/// * `precision` - How many digits of precision, if any. /// * `padding_char` - The padding character as determined by `determine_padding_char`. fn print_unsigned( num: u64, flags: &Flags, width: usize, - precision: Option, + precision: Precision, padding_char: Padding, ) { let num = num.to_string(); @@ -351,7 +529,11 @@ fn print_unsigned( } else { Cow::Borrowed(num.as_str()) }; - let s = format!("{s:0>precision$}", precision = precision.unwrap_or(0)); + let s = match precision { + Precision::NotSpecified => s, + Precision::NoNumber => s, + Precision::Number(p) => format!("{s:0>precision$}", precision = p).into(), + }; pad_and_print(&s, flags.left, width, padding_char); } @@ -362,20 +544,21 @@ fn print_unsigned( /// * `num` - The unsigned octal integer value to be printed. /// * `flags` - A reference to the Flags struct containing formatting flags. /// * `width` - The width of the field for the printed unsigned octal integer. -/// * `precision` - An Option containing the precision value. +/// * `precision` - How many digits of precision, if any. /// * `padding_char` - The padding character as determined by `determine_padding_char`. fn print_unsigned_oct( num: u32, flags: &Flags, width: usize, - precision: Option, + precision: Precision, padding_char: Padding, ) { let prefix = if flags.alter { "0" } else { "" }; - let s = format!( - "{prefix}{num:0>precision$o}", - precision = precision.unwrap_or(0) - ); + let s = match precision { + Precision::NotSpecified => format!("{prefix}{num:o}"), + Precision::NoNumber => format!("{prefix}{num:o}"), + Precision::Number(p) => format!("{prefix}{num:0>precision$o}", precision = p), + }; pad_and_print(&s, flags.left, width, padding_char); } @@ -386,24 +569,48 @@ fn print_unsigned_oct( /// * `num` - The unsigned hexadecimal integer value to be printed. /// * `flags` - A reference to the Flags struct containing formatting flags. /// * `width` - The width of the field for the printed unsigned hexadecimal integer. -/// * `precision` - An Option containing the precision value. +/// * `precision` - How many digits of precision, if any. /// * `padding_char` - The padding character as determined by `determine_padding_char`. fn print_unsigned_hex( num: u64, flags: &Flags, width: usize, - precision: Option, + precision: Precision, padding_char: Padding, ) { let prefix = if flags.alter { "0x" } else { "" }; - let s = format!( - "{prefix}{num:0>precision$x}", - precision = precision.unwrap_or(0) - ); + let s = match precision { + Precision::NotSpecified => format!("{prefix}{num:x}"), + Precision::NoNumber => format!("{prefix}{num:x}"), + Precision::Number(p) => format!("{prefix}{num:0>precision$x}", precision = p), + }; 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, + // This is not documented but the behavior seems to be + // the same as a space. For example `stat -c "%I5s" f` + // prints " 0". + 'I' => flag.space = true, + '+' => flag.sign = true, + '\'' => flag.group = true, + _ => break, + } + *i += 1; + } + } + fn handle_percent_case( chars: &[char], i: &mut usize, @@ -423,28 +630,24 @@ 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; + let mut precision = Precision::NotSpecified; let mut j = *i; 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)?; @@ -455,19 +658,37 @@ impl Stater { match format_str[j..].scan_num::() { Some((value, offset)) => { if value >= 0 { - precision = Some(value as usize); + precision = Precision::Number(value as usize); } j += offset; } - None => precision = Some(0), + None => precision = Precision::NoNumber, } check_bound(format_str, bound, old, j)?; } *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 +706,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) - } else { - show_warning!("unrecognized escape '\\x'"); - Token::Char('x') + '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) } - '0'..='7' => { - let (c, offset) = format_str[*i..].scan_char(8).unwrap(); - *i += offset - 1; - Token::Char(c) + '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!("incomplete hex escape '\\x'"); + Token::Byte(b'x') + } } - '"' => 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 +871,145 @@ 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' => { + let sec = meta.mtime(); + let nsec = meta.mtime_nsec(); + let tm = + chrono::DateTime::from_timestamp(sec, nsec as u32).unwrap_or_default(); + let tm: DateTime = tm.into(); + match tm.timestamp_nanos_opt() { + None => { + let micros = tm.timestamp_micros(); + let secs = micros as f64 / 1_000_000.0; + OutputType::Float(secs) + } + Some(ns) => { + let secs = ns as f64 / 1_000_000_000.0; + OutputType::Float(secs) + } + } + } + // 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 +1034,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 +1066,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; } } } @@ -969,7 +1197,7 @@ fn pretty_time(sec: i64, nsec: i64) -> String { #[cfg(test)] mod tests { - use super::{group_num, Flags, ScanUtil, Stater, Token}; + use super::{group_num, precision_trunc, Flags, Precision, ScanUtil, Stater, Token}; #[test] fn test_scanners() { @@ -1017,7 +1245,7 @@ mod tests { ..Default::default() }, width: 10, - precision: Some(2), + precision: Precision::Number(2), format: 'a', }, Token::Char('c'), @@ -1028,7 +1256,7 @@ mod tests { ..Default::default() }, width: 5, - precision: Some(0), + precision: Precision::NoNumber, format: 'w', }, Token::Char('\n'), @@ -1038,7 +1266,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 { @@ -1048,18 +1276,18 @@ mod tests { ..Default::default() }, width: 15, - precision: None, + precision: Precision::NotSpecified, 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, @@ -1067,15 +1295,27 @@ mod tests { ..Default::default() }, width: 20, - precision: None, + precision: Precision::NotSpecified, 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()); } + + #[test] + fn test_precision_trunc() { + assert_eq!(precision_trunc(123.456, Precision::NotSpecified), "123"); + assert_eq!(precision_trunc(123.456, Precision::NoNumber), "123.456"); + assert_eq!(precision_trunc(123.456, Precision::Number(0)), "123"); + assert_eq!(precision_trunc(123.456, Precision::Number(1)), "123.4"); + assert_eq!(precision_trunc(123.456, Precision::Number(2)), "123.45"); + assert_eq!(precision_trunc(123.456, Precision::Number(3)), "123.456"); + assert_eq!(precision_trunc(123.456, Precision::Number(4)), "123.4560"); + assert_eq!(precision_trunc(123.456, Precision::Number(5)), "123.45600"); + } } diff --git a/src/uu/stdbuf/Cargo.toml b/src/uu/stdbuf/Cargo.toml index 0e17c3441e3..75af9db3960 100644 --- a/src/uu/stdbuf/Cargo.toml +++ b/src/uu/stdbuf/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_stdbuf" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "stdbuf ~ (uutils) run COMMAND with modified standard stream buffering" @@ -22,7 +22,7 @@ tempfile = { workspace = true } uucore = { workspace = true } [build-dependencies] -libstdbuf = { version = "0.0.28", package = "uu_stdbuf_libstdbuf", path = "src/libstdbuf" } +libstdbuf = { version = "0.0.29", package = "uu_stdbuf_libstdbuf", path = "src/libstdbuf" } [[bin]] name = "stdbuf" diff --git a/src/uu/stdbuf/src/libstdbuf/Cargo.toml b/src/uu/stdbuf/src/libstdbuf/Cargo.toml index ff9de77fcc9..a49832b3434 100644 --- a/src/uu/stdbuf/src/libstdbuf/Cargo.toml +++ b/src/uu/stdbuf/src/libstdbuf/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_stdbuf_libstdbuf" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "stdbuf/libstdbuf ~ (uutils); dynamic library required for stdbuf" @@ -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/stdbuf/src/stdbuf.rs b/src/uu/stdbuf/src/stdbuf.rs index bc7b2394911..4540c60d89f 100644 --- a/src/uu/stdbuf/src/stdbuf.rs +++ b/src/uu/stdbuf/src/stdbuf.rs @@ -157,9 +157,24 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { set_command_env(&mut command, "_STDBUF_E", &options.stderr); command.args(command_params); - let mut process = command - .spawn() - .map_err_context(|| "failed to execute process".to_string())?; + const EXEC_ERROR: &str = "failed to execute process:"; + let mut process = match command.spawn() { + Ok(p) => p, + Err(e) => { + return match e.kind() { + std::io::ErrorKind::PermissionDenied => Err(USimpleError::new( + 126, + format!("{EXEC_ERROR} Permission denied"), + )), + std::io::ErrorKind::NotFound => Err(USimpleError::new( + 127, + format!("{EXEC_ERROR} No such file or directory"), + )), + _ => Err(USimpleError::new(1, format!("{EXEC_ERROR} {}", e))), + } + } + }; + let status = process.wait().map_err_context(String::new)?; match status.code() { Some(i) => { diff --git a/src/uu/stty/Cargo.toml b/src/uu/stty/Cargo.toml index c38df5819d9..7d34d13f1d4 100644 --- a/src/uu/stty/Cargo.toml +++ b/src/uu/stty/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_stty" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "stty ~ (uutils) print or change terminal characteristics" diff --git a/src/uu/sum/Cargo.toml b/src/uu/sum/Cargo.toml index 548bc44e072..1995f11df85 100644 --- a/src/uu/sum/Cargo.toml +++ b/src/uu/sum/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_sum" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "sum ~ (uutils) display checksum and block counts for input" diff --git a/src/uu/sum/src/sum.rs b/src/uu/sum/src/sum.rs index d1f383351aa..bae288d803f 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/sync/Cargo.toml b/src/uu/sync/Cargo.toml index 5c02cada17e..8ce6cb73adb 100644 --- a/src/uu/sync/Cargo.toml +++ b/src/uu/sync/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_sync" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "sync ~ (uutils) synchronize cache writes to storage" diff --git a/src/uu/tac/Cargo.toml b/src/uu/tac/Cargo.toml index 60d4f3b3e7e..2d9aedb8e40 100644 --- a/src/uu/tac/Cargo.toml +++ b/src/uu/tac/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "uu_tac" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "tac ~ (uutils) concatenate and display input lines in reverse order" diff --git a/src/uu/tac/src/tac.rs b/src/uu/tac/src/tac.rs index 3865c61ae48..d1eca4706a4 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/Cargo.toml b/src/uu/tail/Cargo.toml index d687a6f1d31..011ee31ceb4 100644 --- a/src/uu/tail/Cargo.toml +++ b/src/uu/tail/Cargo.toml @@ -1,7 +1,7 @@ # spell-checker:ignore (libs) kqueue fundu [package] name = "uu_tail" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "tail ~ (uutils) display the last lines of input" diff --git a/src/uu/tail/src/args.rs b/src/uu/tail/src/args.rs index 5cadac60869..24b064d1bfd 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 636de7a90ef..2c80ac0ac01 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 117cab8b0a0..4a680943c11 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 edac4b151cb..a48da6b315e 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)?; + InputKind::Stdin => { + tail_stdin(settings, &mut printer, input, &mut observer)?; } - // File points to /dev/stdin here - InputKind::File(_) | InputKind::Stdin => { + 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/tee/Cargo.toml b/src/uu/tee/Cargo.toml index bee28ffb159..282ae46731e 100644 --- a/src/uu/tee/Cargo.toml +++ b/src/uu/tee/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_tee" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "tee ~ (uutils) display input and copy to FILE" diff --git a/src/uu/test/Cargo.toml b/src/uu/test/Cargo.toml index 6994efeaa5a..16e6376ed69 100644 --- a/src/uu/test/Cargo.toml +++ b/src/uu/test/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_test" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "test ~ (uutils) evaluate comparison and file type expressions" diff --git a/src/uu/timeout/Cargo.toml b/src/uu/timeout/Cargo.toml index 8c5f5dcbe8e..eddcf8222f6 100644 --- a/src/uu/timeout/Cargo.toml +++ b/src/uu/timeout/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_timeout" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "timeout ~ (uutils) run COMMAND with a DURATION time limit" diff --git a/src/uu/timeout/src/timeout.rs b/src/uu/timeout/src/timeout.rs index 19016900ac2..2ba93769aa1 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/touch/Cargo.toml b/src/uu/touch/Cargo.toml index 1aa788c8d9f..b076ddfd882 100644 --- a/src/uu/touch/Cargo.toml +++ b/src/uu/touch/Cargo.toml @@ -1,7 +1,7 @@ # spell-checker:ignore datetime [package] name = "uu_touch" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "touch ~ (uutils) change FILE timestamps" diff --git a/src/uu/tr/Cargo.toml b/src/uu/tr/Cargo.toml index 0787e427987..a9a0e2089b6 100644 --- a/src/uu/tr/Cargo.toml +++ b/src/uu/tr/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_tr" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "tr ~ (uutils) translate characters within input and display" @@ -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 fc01a83608d..6a1bf939126 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 67998d26d4b..c226d218972 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/true/Cargo.toml b/src/uu/true/Cargo.toml index 6b1657276d7..e9d85a6c941 100644 --- a/src/uu/true/Cargo.toml +++ b/src/uu/true/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_true" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "true ~ (uutils) do nothing and succeed" diff --git a/src/uu/truncate/Cargo.toml b/src/uu/truncate/Cargo.toml index 9c19a8d8ad4..6845ce27d32 100644 --- a/src/uu/truncate/Cargo.toml +++ b/src/uu/truncate/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_truncate" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "truncate ~ (uutils) truncate (or extend) FILE to SIZE" diff --git a/src/uu/tsort/Cargo.toml b/src/uu/tsort/Cargo.toml index 4e179c47f4a..ba53e0d7be8 100644 --- a/src/uu/tsort/Cargo.toml +++ b/src/uu/tsort/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_tsort" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "tsort ~ (uutils) topologically sort input (partially ordered) pairs" diff --git a/src/uu/tsort/src/tsort.rs b/src/uu/tsort/src/tsort.rs index 4a61f271033..aac0a055fea 100644 --- a/src/uu/tsort/src/tsort.rs +++ b/src/uu/tsort/src/tsort.rs @@ -2,16 +2,14 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -//spell-checker:ignore TAOCP +//spell-checker:ignore TAOCP indegree use clap::{crate_version, Arg, Command}; use std::collections::{HashMap, HashSet, VecDeque}; -use std::fmt::Write; -use std::fs::File; -use std::io::{stdin, BufReader, Read}; +use std::fmt::Display; use std::path::Path; use uucore::display::Quotable; -use uucore::error::{FromIo, UResult, USimpleError}; -use uucore::{format_usage, help_about, help_usage}; +use uucore::error::{UError, UResult}; +use uucore::{format_usage, help_about, help_usage, show}; const ABOUT: &str = help_about!("tsort.md"); const USAGE: &str = help_usage!("tsort.md"); @@ -20,6 +18,43 @@ mod options { pub const FILE: &str = "file"; } +#[derive(Debug)] +enum TsortError { + /// The input file is actually a directory. + IsDir(String), + + /// The number of tokens in the input data is odd. + /// + /// The list of edges must be even because each edge has two + /// components: a source node and a target node. + NumTokensOdd(String), + + /// The graph contains a cycle. + Loop(String), + + /// A particular node in a cycle. (This is mainly used for printing.) + LoopNode(String), +} + +impl std::error::Error for TsortError {} + +impl UError for TsortError {} + +impl Display for TsortError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::IsDir(d) => write!(f, "{d}: read error: Is a directory"), + Self::NumTokensOdd(i) => write!( + f, + "{}: input contains an odd number of tokens", + i.maybe_quote() + ), + Self::Loop(i) => write!(f, "{i}: input contains a loop:"), + Self::LoopNode(v) => write!(f, "{v}"), + } + } +} + #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uu_app().try_get_matches_from(args)?; @@ -28,67 +63,28 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { .get_one::(options::FILE) .expect("Value is required by clap"); - let mut stdin_buf; - let mut file_buf; - let mut reader = BufReader::new(if input == "-" { - stdin_buf = stdin(); - &mut stdin_buf as &mut dyn Read + let data = if input == "-" { + let stdin = std::io::stdin(); + std::io::read_to_string(stdin)? } else { let path = Path::new(&input); if path.is_dir() { - return Err(USimpleError::new( - 1, - format!("{input}: read error: Is a directory"), - )); - } - file_buf = File::open(path).map_err_context(|| input.to_string())?; - &mut file_buf as &mut dyn Read - }); - - let mut input_buffer = String::new(); - reader.read_to_string(&mut input_buffer)?; - let mut g = Graph::default(); - - for line in input_buffer.lines() { - let tokens: Vec<_> = line.split_whitespace().collect(); - if tokens.is_empty() { - break; + return Err(TsortError::IsDir(input.to_string()).into()); } - for ab in tokens.chunks(2) { - match ab.len() { - 2 => g.add_edge(ab[0], ab[1]), - _ => { - return Err(USimpleError::new( - 1, - format!( - "{}: input contains an odd number of tokens", - input.maybe_quote() - ), - )) - } - } - } - } + std::fs::read_to_string(path)? + }; - match g.run_tsort() { - Err(cycle) => { - let mut error_message = format!( - "{}: {}: input contains a loop:\n", - uucore::util_name(), - input - ); - for node in &cycle { - writeln!(error_message, "{}: {}", uucore::util_name(), node).unwrap(); - } - eprint!("{}", error_message); - println!("{}", cycle.join("\n")); - Err(USimpleError::new(1, "")) - } - Ok(ordering) => { - println!("{}", ordering.join("\n")); - Ok(()) + // Create the directed graph from pairs of tokens in the input data. + let mut g = Graph::new(input.clone()); + for ab in data.split_whitespace().collect::>().chunks(2) { + match ab { + [a, b] => g.add_edge(a, b), + _ => return Err(TsortError::NumTokensOdd(input.to_string()).into()), } } + + g.run_tsort(); + Ok(()) } pub fn uu_app() -> Command { Command::new(uucore::util_name()) @@ -104,34 +100,45 @@ pub fn uu_app() -> Command { ) } +/// Find the element `x` in `vec` and remove it, returning its index. +fn remove(vec: &mut Vec, x: T) -> Option +where + T: PartialEq, +{ + vec.iter().position(|item| *item == x).inspect(|i| { + vec.remove(*i); + }) +} + // We use String as a representation of node here // but using integer may improve performance. - +#[derive(Default)] struct Node<'input> { successor_names: Vec<&'input str>, predecessor_count: usize, } impl<'input> Node<'input> { - fn new() -> Self { - Node { - successor_names: Vec::new(), - predecessor_count: 0, - } - } - fn add_successor(&mut self, successor_name: &'input str) { self.successor_names.push(successor_name); } } -#[derive(Default)] + struct Graph<'input> { + name: String, nodes: HashMap<&'input str, Node<'input>>, } impl<'input> Graph<'input> { + fn new(name: String) -> Graph<'input> { + Self { + name, + nodes: HashMap::default(), + } + } + fn add_node(&mut self, name: &'input str) { - self.nodes.entry(name).or_insert_with(Node::new); + self.nodes.entry(name).or_default(); } fn add_edge(&mut self, from: &'input str, to: &'input str) { @@ -146,9 +153,14 @@ impl<'input> Graph<'input> { to_node.predecessor_count += 1; } } + + fn remove_edge(&mut self, u: &'input str, v: &'input str) { + remove(&mut self.nodes.get_mut(u).unwrap().successor_names, v); + self.nodes.get_mut(v).unwrap().predecessor_count -= 1; + } + /// Implementation of algorithm T from TAOCP (Don. Knuth), vol. 1. - fn run_tsort(&mut self) -> Result, Vec<&'input str>> { - let mut result = Vec::with_capacity(self.nodes.len()); + fn run_tsort(&mut self) { // First, we find a node that have no prerequisites (independent nodes) // If no such node exists, then there is a cycle. let mut independent_nodes_queue: VecDeque<&'input str> = self @@ -165,10 +177,18 @@ impl<'input> Graph<'input> { independent_nodes_queue.make_contiguous().sort_unstable(); // to make sure the resulting ordering is deterministic we need to order independent nodes // FIXME: this doesn't comply entirely with the GNU coreutils implementation. - // we remove each independent node, from the graph, updating each successor predecessor_count variable as we do. - while let Some(name_of_next_node_to_process) = independent_nodes_queue.pop_front() { - result.push(name_of_next_node_to_process); - if let Some(node_to_process) = self.nodes.remove(name_of_next_node_to_process) { + // To make sure the resulting ordering is deterministic we + // need to order independent nodes. + // + // FIXME: this doesn't comply entirely with the GNU coreutils + // implementation. + independent_nodes_queue.make_contiguous().sort_unstable(); + + while !self.nodes.is_empty() { + // Get the next node (breaking any cycles necessary to do so). + let v = self.find_next_node(&mut independent_nodes_queue); + println!("{v}"); + if let Some(node_to_process) = self.nodes.remove(v) { for successor_name in node_to_process.successor_names { let successor_node = self.nodes.get_mut(successor_name).unwrap(); successor_node.predecessor_count -= 1; @@ -179,20 +199,61 @@ impl<'input> Graph<'input> { } } } + } + + /// Get the in-degree of the node with the given name. + fn indegree(&self, name: &str) -> Option { + self.nodes.get(name).map(|data| data.predecessor_count) + } - // if the graph has no cycle (it's a dependency tree), the graph should be empty now, as all nodes have been deleted. - if self.nodes.is_empty() { - Ok(result) - } else { - // otherwise, we detect and show a cycle to the user (as the GNU coreutils implementation does) - Err(self.detect_cycle()) + // Pre-condition: self.nodes is non-empty. + fn find_next_node(&mut self, frontier: &mut VecDeque<&'input str>) -> &'input str { + // If there are no nodes of in-degree zero but there are still + // un-visited nodes in the graph, then there must be a cycle. + // We need to find the cycle, display it, and then break the + // cycle. + // + // A cycle is guaranteed to be of length at least two. We break + // the cycle by deleting an arbitrary edge (the first). That is + // not necessarily the optimal thing, but it should be enough to + // continue making progress in the graph traversal. + // + // It is possible that deleting the edge does not actually + // result in the target node having in-degree zero, so we repeat + // the process until such a node appears. + loop { + match frontier.pop_front() { + None => self.find_and_break_cycle(frontier), + Some(v) => return v, + } + } + } + + fn find_and_break_cycle(&mut self, frontier: &mut VecDeque<&'input str>) { + let cycle = self.detect_cycle(); + show!(TsortError::Loop(self.name.clone())); + for node in &cycle { + show!(TsortError::LoopNode(node.to_string())); + } + let u = cycle[0]; + let v = cycle[1]; + self.remove_edge(u, v); + if self.indegree(v).unwrap() == 0 { + frontier.push_back(v); } } fn detect_cycle(&self) -> Vec<&'input str> { + // Sort the nodes just to make this function deterministic. + let mut nodes = Vec::new(); + for node in self.nodes.keys() { + nodes.push(node); + } + nodes.sort_unstable(); + let mut visited = HashSet::new(); let mut stack = Vec::with_capacity(self.nodes.len()); - for &node in self.nodes.keys() { + for node in nodes { if !visited.contains(node) && self.dfs(node, &mut visited, &mut stack) { return stack; } diff --git a/src/uu/tty/Cargo.toml b/src/uu/tty/Cargo.toml index 9f81a69cc3c..dac2464d4b7 100644 --- a/src/uu/tty/Cargo.toml +++ b/src/uu/tty/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_tty" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "tty ~ (uutils) display the name of the terminal connected to standard input" diff --git a/src/uu/uname/Cargo.toml b/src/uu/uname/Cargo.toml index 87446827314..5545445a1a0 100644 --- a/src/uu/uname/Cargo.toml +++ b/src/uu/uname/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_uname" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "uname ~ (uutils) display system information" diff --git a/src/uu/unexpand/Cargo.toml b/src/uu/unexpand/Cargo.toml index f6725762d29..b0ed1fa845d 100644 --- a/src/uu/unexpand/Cargo.toml +++ b/src/uu/unexpand/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_unexpand" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "unexpand ~ (uutils) convert input spaces to tabs" diff --git a/src/uu/unexpand/src/unexpand.rs b/src/uu/unexpand/src/unexpand.rs index 7336376eb6c..1e8cede37dd 100644 --- a/src/uu/unexpand/src/unexpand.rs +++ b/src/uu/unexpand/src/unexpand.rs @@ -16,7 +16,7 @@ use std::str::from_utf8; use unicode_width::UnicodeWidthChar; use uucore::display::Quotable; use uucore::error::{FromIo, UError, UResult, USimpleError}; -use uucore::{crash_if_err, format_usage, help_about, help_usage, show}; +use uucore::{format_usage, help_about, help_usage, show}; const USAGE: &str = help_usage!("unexpand.md"); const ABOUT: &str = help_about!("unexpand.md"); @@ -244,7 +244,7 @@ fn write_tabs( prevtab: bool, init: bool, amode: bool, -) { +) -> UResult<()> { // This conditional establishes the following: // We never turn a single space before a non-blank into // a tab, unless it's at the start of the line. @@ -255,15 +255,16 @@ fn write_tabs( break; } - crash_if_err!(1, output.write_all(b"\t")); + output.write_all(b"\t")?; scol += nts; } } while col > scol { - crash_if_err!(1, output.write_all(b" ")); + output.write_all(b" ")?; scol += 1; } + Ok(()) } #[derive(PartialEq, Eq, Debug)] @@ -325,7 +326,7 @@ fn unexpand_line( options: &Options, lastcol: usize, ts: &[usize], -) -> std::io::Result<()> { +) -> UResult<()> { let mut byte = 0; // offset into the buffer let mut col = 0; // the current column let mut scol = 0; // the start col for the current span, i.e., the already-printed width @@ -335,7 +336,7 @@ fn unexpand_line( while byte < buf.len() { // when we have a finite number of columns, never convert past the last column if lastcol > 0 && col >= lastcol { - write_tabs(output, ts, scol, col, pctype == CharType::Tab, init, true); + write_tabs(output, ts, scol, col, pctype == CharType::Tab, init, true)?; output.write_all(&buf[byte..])?; scol = col; break; @@ -370,7 +371,7 @@ fn unexpand_line( pctype == CharType::Tab, init, options.aflag, - ); + )?; init = false; // no longer at the start of a line col = if ctype == CharType::Other { // use computed width @@ -391,7 +392,7 @@ fn unexpand_line( } // write out anything remaining - write_tabs(output, ts, scol, col, pctype == CharType::Tab, init, true); + write_tabs(output, ts, scol, col, pctype == CharType::Tab, init, true)?; output.flush()?; buf.truncate(0); // clear out the buffer diff --git a/src/uu/uniq/Cargo.toml b/src/uu/uniq/Cargo.toml index 9d23e06b0bd..ace29f4701f 100644 --- a/src/uu/uniq/Cargo.toml +++ b/src/uu/uniq/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_uniq" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "uniq ~ (uutils) filter identical adjacent lines from input" diff --git a/src/uu/uniq/src/uniq.rs b/src/uu/uniq/src/uniq.rs index 4084a7b3f22..4995f8c198e 100644 --- a/src/uu/uniq/src/uniq.rs +++ b/src/uu/uniq/src/uniq.rs @@ -154,43 +154,44 @@ impl Uniq { fn cmp_key(&self, line: &[u8], mut closure: F) -> bool where - F: FnMut(&mut dyn Iterator) -> bool, + F: FnMut(&mut dyn Iterator) -> bool, { let fields_to_check = self.skip_fields(line); - let len = fields_to_check.len(); - let slice_start = self.slice_start.unwrap_or(0); - let slice_stop = self.slice_stop.unwrap_or(len); - if len > 0 { - // fast path: avoid doing any work if there is no need to skip or map to lower-case - if !self.ignore_case && slice_start == 0 && slice_stop == len { - return closure(&mut fields_to_check.iter().copied()); - } - // fast path: avoid skipping - if self.ignore_case && slice_start == 0 && slice_stop == len { - return closure(&mut fields_to_check.iter().map(|u| u.to_ascii_lowercase())); - } + // Skip self.slice_start bytes (if -s was used). + // self.slice_start is how many characters to skip, but historically + // uniq’s `-s N` means “skip N *bytes*,” so do that literally: + let skip_bytes = self.slice_start.unwrap_or(0); + let fields_to_check = if skip_bytes < fields_to_check.len() { + &fields_to_check[skip_bytes..] + } else { + // If skipping beyond end-of-line, leftover is empty => effectively "" + &[] + }; - // fast path: we can avoid mapping chars to lower-case, if we don't want to ignore the case - if !self.ignore_case { - return closure( - &mut fields_to_check - .iter() - .skip(slice_start) - .take(slice_stop) - .copied(), - ); + // Convert the leftover bytes to UTF-8 for character-based -w + // If invalid UTF-8, just compare them as individual bytes (fallback). + let string_after_skip = match std::str::from_utf8(fields_to_check) { + Ok(s) => s, + Err(_) => { + // Fallback: if invalid UTF-8, treat them as single-byte “chars” + return closure(&mut fields_to_check.iter().map(|&b| b as char)); } + }; - closure( - &mut fields_to_check - .iter() - .skip(slice_start) - .take(slice_stop) - .map(|u| u.to_ascii_lowercase()), - ) + let total_chars = string_after_skip.chars().count(); + + // `-w N` => Compare no more than N characters + let slice_stop = self.slice_stop.unwrap_or(total_chars); + let slice_start = slice_stop.min(total_chars); + + let mut iter = string_after_skip.chars().take(slice_start); + + if self.ignore_case { + // We can do ASCII-lowercase or full Unicode-lowercase. For minimal changes, do ASCII: + closure(&mut iter.map(|c| c.to_ascii_lowercase())) } else { - closure(&mut fields_to_check.iter().copied()) + closure(&mut iter) } } @@ -383,7 +384,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/unlink/Cargo.toml b/src/uu/unlink/Cargo.toml index 380bddcbe73..3152ccd4597 100644 --- a/src/uu/unlink/Cargo.toml +++ b/src/uu/unlink/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_unlink" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "unlink ~ (uutils) remove a (file system) link to FILE" diff --git a/src/uu/uptime/Cargo.toml b/src/uu/uptime/Cargo.toml index bae87dfd3c8..38126a3e956 100644 --- a/src/uu/uptime/Cargo.toml +++ b/src/uu/uptime/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_uptime" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "uptime ~ (uutils) display dynamic system information" diff --git a/src/uu/users/Cargo.toml b/src/uu/users/Cargo.toml index 1f1f5773d28..fa9f4c8271b 100644 --- a/src/uu/users/Cargo.toml +++ b/src/uu/users/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_users" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "users ~ (uutils) display names of currently logged-in users" diff --git a/src/uu/vdir/Cargo.toml b/src/uu/vdir/Cargo.toml index f76f4c96b0f..09fc48c05a8 100644 --- a/src/uu/vdir/Cargo.toml +++ b/src/uu/vdir/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_vdir" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "shortcut to ls -l -b" diff --git a/src/uu/wc/Cargo.toml b/src/uu/wc/Cargo.toml index 193e1667915..b3e06e29681 100644 --- a/src/uu/wc/Cargo.toml +++ b/src/uu/wc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_wc" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "wc ~ (uutils) display newline, word, and byte counts for input" diff --git a/src/uu/wc/src/utf8/read.rs b/src/uu/wc/src/utf8/read.rs index 819b0a6891c..9515cdc9fe6 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 33b70ee62f5..6fc1efa0a00 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, }; @@ -255,13 +255,17 @@ impl<'a> Input<'a> { } /// Converts input to title that appears in stats. - fn to_title(&self) -> Option> { + fn to_title(&self) -> Option> { 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)), - }), - Self::Stdin(StdinKind::Explicit) => Some(Cow::Borrowed(STDIN_REPR)), + Self::Path(path) => { + let path = path.as_os_str(); + if path.to_string_lossy().contains('\n') { + Some(Cow::Owned(quoting_style::escape_name(path, QS_ESCAPE))) + } else { + Some(Cow::Borrowed(path)) + } + } + Self::Stdin(StdinKind::Explicit) => Some(Cow::Borrowed(OsStr::new(STDIN_REPR))), Self::Stdin(StdinKind::Implicit) => None, } } @@ -269,7 +273,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 +365,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 +765,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 +799,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 +814,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; @@ -844,14 +856,17 @@ fn wc(inputs: &Inputs, settings: &Settings) -> UResult<()> { let maybe_title = input.to_title(); let maybe_title_str = maybe_title.as_deref(); if let Err(err) = print_stats(settings, &word_count, maybe_title_str, number_width) { - let title = maybe_title_str.unwrap_or(""); - show!(err.map_err_context(|| format!("failed to print result for {title}"))); + let title = maybe_title_str.unwrap_or(OsStr::new("")); + show!(err.map_err_context(|| format!( + "failed to print result for {}", + title.to_string_lossy() + ))); } } } if settings.total_when.is_total_row_visible(num_inputs) { - let title = are_stats_visible.then_some("total"); + let title = are_stats_visible.then_some(OsStr::new("total")); if let Err(err) = print_stats(settings, &total_word_count, title, number_width) { show!(err.map_err_context(|| "failed to print total".into())); } @@ -865,7 +880,7 @@ fn wc(inputs: &Inputs, settings: &Settings) -> UResult<()> { fn print_stats( settings: &Settings, result: &WordCount, - title: Option<&str>, + title: Option<&OsStr>, number_width: usize, ) -> io::Result<()> { let mut stdout = io::stdout().lock(); @@ -885,8 +900,8 @@ fn print_stats( } if let Some(title) = title { - writeln!(stdout, "{space}{title}") - } else { - writeln!(stdout) + write!(stdout, "{space}")?; + stdout.write_all(&uucore::os_str_as_bytes_lossy(title))?; } + writeln!(stdout) } diff --git a/src/uu/who/Cargo.toml b/src/uu/who/Cargo.toml index f5aab2ecd24..15bed98b70e 100644 --- a/src/uu/who/Cargo.toml +++ b/src/uu/who/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_who" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "who ~ (uutils) display information about currently logged-in users" diff --git a/src/uu/whoami/Cargo.toml b/src/uu/whoami/Cargo.toml index 4f3f7659905..7b24429b0d4 100644 --- a/src/uu/whoami/Cargo.toml +++ b/src/uu/whoami/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_whoami" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "whoami ~ (uutils) display user name of current effective user ID" diff --git a/src/uu/yes/Cargo.toml b/src/uu/yes/Cargo.toml index 21b5ff7ac33..af1b937b79f 100644 --- a/src/uu/yes/Cargo.toml +++ b/src/uu/yes/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_yes" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "yes ~ (uutils) repeatedly display a line with STRING (or 'y')" diff --git a/src/uucore/Cargo.toml b/src/uucore/Cargo.toml index b72a8ed717c..ee461e048ce 100644 --- a/src/uucore/Cargo.toml +++ b/src/uucore/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "uucore" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "uutils ~ 'core' uutils code library (cross-platform)" @@ -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 cf24637f7bf..ef5be724d9f 100644 --- a/src/uucore/src/lib/features.rs +++ b/src/uucore/src/lib/features.rs @@ -6,6 +6,8 @@ #[cfg(feature = "backup-control")] pub mod backup_control; +#[cfg(feature = "buf-copy")] +pub mod buf_copy; #[cfg(feature = "checksum")] pub mod checksum; #[cfg(feature = "colors")] @@ -43,7 +45,7 @@ pub mod mode; 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 9086acb197e..591f57f95f4 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,41 @@ 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 00000000000..16138e67fa2 --- /dev/null +++ b/src/uucore/src/lib/features/buf_copy.rs @@ -0,0 +1,118 @@ +// 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). + +pub mod common; + +#[cfg(any(target_os = "linux", target_os = "android"))] +pub mod linux; +#[cfg(any(target_os = "linux", target_os = "android"))] +pub use linux::*; + +#[cfg(not(any(target_os = "linux", target_os = "android")))] +pub mod other; +#[cfg(not(any(target_os = "linux", target_os = "android")))] +pub use other::copy_stream; + +#[cfg(test)] +mod tests { + use super::*; + use std::fs::File; + use tempfile::tempdir; + + #[cfg(unix)] + use { + crate::pipes, + std::fs::OpenOptions, + std::{ + io::{Seek, SeekFrom}, + thread, + }, + }; + + #[cfg(any(target_os = "linux", target_os = "android"))] + use std::os::fd::AsRawFd; + + use std::io::{Read, Write}; + + #[cfg(unix)] + fn new_temp_file() -> File { + let temp_dir = tempdir().unwrap(); + OpenOptions::new() + .read(true) + .write(true) + .create(true) + .open(temp_dir.path().join("file.txt")) + .unwrap() + } + + #[cfg(any(target_os = "linux", target_os = "android"))] + #[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] + #[cfg(unix)] + fn test_copy_stream() { + let mut dest_file = new_temp_file(); + + let (mut pipe_read, mut pipe_write) = pipes::pipe().unwrap(); + let data = b"Hello, world!"; + let thread = thread::spawn(move || { + pipe_write.write_all(data).unwrap(); + }); + let result = copy_stream(&mut pipe_read, &mut dest_file).unwrap(); + thread.join().unwrap(); + assert!(result == data.len() as u64); + + // We would have been at the end already, so seek again to the start. + dest_file.seek(SeekFrom::Start(0)).unwrap(); + + let mut buf = Vec::new(); + dest_file.read_to_end(&mut buf).unwrap(); + + assert_eq!(buf, data); + } + + #[test] + #[cfg(not(unix))] + // Test for non-unix platforms. We use regular files instead. + fn test_copy_stream() { + let temp_dir = tempdir().unwrap(); + let src_path = temp_dir.path().join("src.txt"); + let dest_path = temp_dir.path().join("dest.txt"); + + let mut src_file = File::create(&src_path).unwrap(); + let mut dest_file = File::create(&dest_path).unwrap(); + + let data = b"Hello, world!"; + src_file.write_all(data).unwrap(); + src_file.sync_all().unwrap(); + + let mut src_file = File::open(&src_path).unwrap(); + let bytes_copied = copy_stream(&mut src_file, &mut dest_file).unwrap(); + + let mut dest_file = File::open(&dest_path).unwrap(); + let mut buf = Vec::new(); + dest_file.read_to_end(&mut buf).unwrap(); + + assert_eq!(bytes_copied as usize, data.len()); + assert_eq!(buf, data); + } +} diff --git a/src/uucore/src/lib/features/buf_copy/common.rs b/src/uucore/src/lib/features/buf_copy/common.rs new file mode 100644 index 00000000000..8c74dbb8a88 --- /dev/null +++ b/src/uucore/src/lib/features/buf_copy/common.rs @@ -0,0 +1,34 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use crate::error::UError; + +/// Error types used by buffer-copying functions from the `buf_copy` module. +#[derive(Debug)] +pub enum Error { + Io(std::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 + } +} diff --git a/src/uucore/src/lib/features/buf_copy/linux.rs b/src/uucore/src/lib/features/buf_copy/linux.rs new file mode 100644 index 00000000000..7ae5b2bd023 --- /dev/null +++ b/src/uucore/src/lib/features/buf_copy/linux.rs @@ -0,0 +1,144 @@ +// 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. + +//! Buffer-based copying implementation for Linux and Android. + +use crate::{ + error::UResult, + pipes::{pipe, splice, splice_exact}, +}; + +/// Buffer-based copying utilities for unix (excluding Linux). +use std::{ + io::{Read, Write}, + os::fd::{AsFd, AsRawFd, RawFd}, +}; + +use super::common::Error; + +/// A readable file descriptor. +pub trait FdReadable: Read + AsRawFd + AsFd {} + +impl FdReadable for T where T: Read + AsFd + AsRawFd {} + +/// A writable file descriptor. +pub trait FdWritable: Write + AsFd + AsRawFd {} + +impl FdWritable for T where T: Write + AsFd + AsRawFd {} + +const SPLICE_SIZE: usize = 1024 * 128; +const BUF_SIZE: usize = 1024 * 16; + +/// Conversion from a `nix::Error` into our `Error` which implements `UError`. +impl From for Error { + fn from(error: nix::Error) -> Self { + Self::Io(std::io::Error::from_raw_os_error(error as i32)) + } +} + +/// 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` 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, +{ + // 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 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] +pub(crate) 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. +#[cfg(any(target_os = "linux", target_os = "android"))] +pub(crate) fn copy_exact( + read_fd: RawFd, + write_fd: &impl AsFd, + num_bytes: usize, +) -> std::io::Result { + use nix::unistd; + + 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) +} diff --git a/src/uucore/src/lib/features/buf_copy/other.rs b/src/uucore/src/lib/features/buf_copy/other.rs new file mode 100644 index 00000000000..61dd13e6cea --- /dev/null +++ b/src/uucore/src/lib/features/buf_copy/other.rs @@ -0,0 +1,32 @@ +// 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. +//! +//! Buffer-based copying implementation for other platforms. + +use std::io::{Read, Write}; + +use crate::error::UResult; + +/// 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, using std::io::copy. This is implemented for +/// non-Linux platforms. +/// +/// # 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, + S: Write, +{ + let result = std::io::copy(src, dest)?; + Ok(result) +} diff --git a/src/uucore/src/lib/features/checksum.rs b/src/uucore/src/lib/features/checksum.rs index a2de28bc560..0b3e4e24938 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), - ]; - - 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)); - } - } +/// 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; + + // 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 -} - -// 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 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)); + } -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 - } + // 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 } - 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)) } -/*** - * 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, -) -> 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; - } - } - }; +/// 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) + } +} - let reader = BufReader::new(file); - let lines = read_os_string_lines(reader).collect::>(); +/// 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 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; - }; + let (algo_name, algo_byte_len) = identify_algo_name_and_length(line_info, cli_algo_name) + .ok_or(LineCheckError::ImproperlyFormatted)?; - 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; + // 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 mut filename_to_check = caps.name("filename").unwrap().as_bytes(); + let expected_checksum = get_expected_digest_as_hex_string(line_info, digest_char_length_hint) + .ok_or(LineCheckError::ImproperlyFormatted)?; - 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 algo = detect_algo(&algo_name, algo_byte_len)?; - 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 - ); - } + compute_and_check_digest_from_file(filename_to_check, &expected_checksum, algo, opts) +} - res.bad_format += 1; - } - } +/// 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) + }; - // 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); + let algo = detect_algo(&algo_name, algo_byte_len)?; - return Ok(()); - } + compute_and_check_digest_from_file(filename_to_check, &expected_checksum, algo, opts) +} - // if any incorrectly formatted line, show it - cksum_output(&res, status); +/// 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); + } - if ignore_missing && correct_format == 0 { - // we have only bad format - // and we had ignore-missing + // 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!( - "{}: {}: no file was verified", + "{}: {}: {}: improperly formatted {} checksum line", util_name(), - filename_input.maybe_quote(), + &filename_input.maybe_quote(), + i + 1, + algo ); - set_exit_code(1); } - // strict means that we should have an exit code. - if strict && res.bad_format > 0 { - set_exit_code(1); + 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, + }; + } - // 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); + // 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) +*/ +pub fn perform_checksum_validation<'a, I>( + files: I, + 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 { + 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())); - - // 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())); + 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()); + + 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 f0573943133..885ae2fe967 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 d1c9f9c046b..f3d1232eb59 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 75851049895..5cdd0342122 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 6d7a2ee3079..6a09b32e2a9 100644 --- a/src/uucore/src/lib/features/format/mod.rs +++ b/src/uucore/src/lib/features/format/mod.rs @@ -22,7 +22,7 @@ //! 3. Parse both `printf` specifiers and escape sequences (for e.g. `printf`) //! //! This module aims to combine all three use cases. An iterator parsing each -//! of these cases is provided by [`parse_escape_only`], [`parse_spec_only`] +//! of these cases is provided by [`parse_spec_only`], [`parse_escape_only`] //! and [`parse_spec_and_escape`], respectively. //! //! There is a special [`Format`] type, which can be used to parse a format @@ -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, @@ -46,9 +46,11 @@ use std::{ ops::ControlFlow, }; +use os_display::Quotable; + use crate::error::UError; -use self::{ +pub use self::{ escape::{parse_escape_code, EscapedChar}, num_format::Formatter, }; @@ -63,6 +65,8 @@ pub enum FormatError { NeedAtLeastOneSpec(Vec), WrongSpecType, InvalidPrecision(String), + /// The format specifier ends with a %, as in `%f%`. + EndsWithPercent(Vec), } impl Error for FormatError {} @@ -92,6 +96,9 @@ impl Display for FormatError { "format '{}' has no % directive", String::from_utf8_lossy(s) ), + Self::EndsWithPercent(s) => { + write!(f, "format {} ends in %", String::from_utf8_lossy(s).quote()) + } Self::InvalidPrecision(precision) => write!(f, "invalid precision: '{precision}'"), // TODO: Error message below needs some work Self::WrongSpecType => write!(f, "wrong % directive type was given"), @@ -190,6 +197,7 @@ pub fn parse_spec_only( let mut current = fmt; std::iter::from_fn(move || match current { [] => None, + [b'%'] => Some(Err(FormatError::EndsWithPercent(fmt.to_vec()))), [b'%', b'%', rest @ ..] => { current = rest; Some(Ok(FormatItem::Char(b'%'))) @@ -323,11 +331,14 @@ impl Format { let mut suffix = Vec::new(); for item in &mut iter { - match item? { - FormatItem::Spec(_) => { + match item { + // If the `format_string` is of the form `%f%f` or + // `%f%`, then return an error. + Ok(FormatItem::Spec(_)) | Err(FormatError::EndsWithPercent(_)) => { return Err(FormatError::TooManySpecs(format_string.as_ref().to_vec())); } - FormatItem::Char(c) => suffix.push(c), + Ok(FormatItem::Char(c)) => suffix.push(c), + Err(e) => return Err(e), } } diff --git a/src/uucore/src/lib/features/format/num_format.rs b/src/uucore/src/lib/features/format/num_format.rs index 546171b96fe..caee8e30374 100644 --- a/src/uucore/src/lib/features/format/num_format.rs +++ b/src/uucore/src/lib/features/format/num_format.rs @@ -404,8 +404,10 @@ fn format_float_shortest( }; } - let mut exponent = f.log10().floor() as i32; - if f != 0.0 && exponent <= -4 || exponent > precision as i32 { + // Retrieve the exponent. Note that log10 is undefined for negative numbers. + // To avoid NaN or zero (due to i32 conversion), use the absolute value of f. + let mut exponent = f.abs().log10().floor() as i32; + if f != 0.0 && exponent < -4 || exponent > precision as i32 { // Scientific-ish notation (with a few differences) let mut normalized = f / 10.0_f64.powi(exponent); @@ -665,4 +667,34 @@ mod test { assert_eq!(&f("1000.02030"), "1000.0203"); assert_eq!(&f("1000.00000"), "1000"); } + + #[test] + fn shortest_float_abs_value_less_than_one() { + use super::format_float_shortest; + let f = |x| format_float_shortest(x, 6, Case::Lowercase, ForceDecimal::No); + assert_eq!(f(0.1171875), "0.117188"); + assert_eq!(f(0.01171875), "0.0117188"); + assert_eq!(f(0.001171875), "0.00117187"); + assert_eq!(f(0.0001171875), "0.000117187"); + assert_eq!(f(0.001171875001), "0.00117188"); + assert_eq!(f(-0.1171875), "-0.117188"); + assert_eq!(f(-0.01171875), "-0.0117188"); + assert_eq!(f(-0.001171875), "-0.00117187"); + assert_eq!(f(-0.0001171875), "-0.000117187"); + assert_eq!(f(-0.001171875001), "-0.00117188"); + } + + #[test] + fn shortest_float_switch_decimal_scientific() { + use super::format_float_shortest; + let f = |x| format_float_shortest(x, 6, Case::Lowercase, ForceDecimal::No); + assert_eq!(f(0.001), "0.001"); + assert_eq!(f(0.0001), "0.0001"); + assert_eq!(f(0.00001), "1e-05"); + assert_eq!(f(0.000001), "1e-06"); + assert_eq!(f(-0.001), "-0.001"); + assert_eq!(f(-0.0001), "-0.0001"); + assert_eq!(f(-0.00001), "-1e-05"); + assert_eq!(f(-0.000001), "-1e-06"); + } } diff --git a/src/uucore/src/lib/features/format/spec.rs b/src/uucore/src/lib/features/format/spec.rs index 581e1fa0624..81dbc1ebc29 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 e0c8ea79d3a..d0875f78a91 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)] @@ -718,7 +719,35 @@ pub fn path_ends_with_terminator(path: &Path) -> bool { path.as_os_str() .encode_wide() .last() - .map_or(false, |wide| wide == b'/'.into() || wide == b'\\'.into()) + .is_some_and(|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 { diff --git a/src/uucore/src/lib/features/fsext.rs b/src/uucore/src/lib/features/fsext.rs index c161db39fc7..fa961388b62 100644 --- a/src/uucore/src/lib/features/fsext.rs +++ b/src/uucore/src/lib/features/fsext.rs @@ -137,6 +137,19 @@ pub struct MountInfo { pub dummy: bool, } +#[cfg(any(target_os = "linux", target_os = "android"))] +fn replace_special_chars(s: String) -> String { + // Replace + // + // * ASCII space with a regular space character, + // * \011 ASCII horizontal tab with a tab character, + // * ASCII backslash with an actual backslash character. + // + s.replace(r#"\040"#, " ") + .replace(r#"\011"#, " ") + .replace(r#"\134"#, r#"\"#) +} + impl MountInfo { #[cfg(any(target_os = "linux", target_os = "android"))] fn new(file_name: &str, raw: &[&str]) -> Option { @@ -158,14 +171,14 @@ impl MountInfo { dev_name = raw[after_fields + 1].to_string(); fs_type = raw[after_fields].to_string(); mount_root = raw[3].to_string(); - mount_dir = raw[4].to_string(); + mount_dir = replace_special_chars(raw[4].to_string()); mount_option = raw[5].to_string(); } LINUX_MTAB => { dev_name = raw[0].to_string(); fs_type = raw[2].to_string(); mount_root = String::new(); - mount_dir = raw[1].to_string(); + mount_dir = replace_special_chars(raw[1].to_string()); mount_option = raw[3].to_string(); } _ => return None, @@ -587,7 +600,7 @@ impl FsUsage { let mut number_of_free_clusters = 0; let mut total_number_of_clusters = 0; - let success = unsafe { + unsafe { let path = to_nul_terminated_wide_string(path); GetDiskFreeSpaceW( path.as_ptr(), @@ -595,15 +608,7 @@ impl FsUsage { &mut bytes_per_sector, &mut number_of_free_clusters, &mut total_number_of_clusters, - ) - }; - if 0 == success { - // Fails in case of CD for example - // crash!( - // EXIT_ERR, - // "GetDiskFreeSpaceW failed: {}", - // IOError::last_os_error() - // ); + ); } let bytes_per_cluster = sectors_per_cluster as u64 * bytes_per_sector as u64; @@ -1089,4 +1094,28 @@ mod tests { assert_eq!(info.fs_type, "xfs"); assert_eq!(info.dev_name, "/dev/fs0"); } + + #[test] + #[cfg(any(target_os = "linux", target_os = "android"))] + fn test_mountinfo_dir_special_chars() { + let info = MountInfo::new( + LINUX_MOUNTINFO, + &r#"317 61 7:0 / /mnt/f\134\040\011oo rw,relatime shared:641 - ext4 /dev/loop0 rw"# + .split_ascii_whitespace() + .collect::>(), + ) + .unwrap(); + + assert_eq!(info.mount_dir, r#"/mnt/f\ oo"#); + + let info = MountInfo::new( + LINUX_MTAB, + &r#"/dev/loop0 /mnt/f\134\040\011oo ext4 rw,relatime 0 0"# + .split_ascii_whitespace() + .collect::>(), + ) + .unwrap(); + + assert_eq!(info.mount_dir, r#"/mnt/f\ oo"#); + } } diff --git a/src/uucore/src/lib/features/perms.rs b/src/uucore/src/lib/features/perms.rs index ebb97042e13..62e7d56ed2f 100644 --- a/src/uucore/src/lib/features/perms.rs +++ b/src/uucore/src/lib/features/perms.rs @@ -13,6 +13,7 @@ pub use crate::features::entries; use crate::show_error; use clap::{Arg, ArgMatches, Command}; use libc::{gid_t, uid_t}; +use options::traverse; use walkdir::WalkDir; use std::io::Error as IOError; @@ -23,7 +24,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)] @@ -33,6 +34,7 @@ pub enum VerbosityLevel { Verbose, Normal, } + #[derive(PartialEq, Eq, Clone, Debug)] pub struct Verbosity { pub groups_only: bool, @@ -214,23 +216,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; } @@ -258,6 +250,14 @@ fn is_root(path: &Path, would_traverse_symlink: bool) -> bool { false } +pub fn get_metadata(file: &Path, follow: bool) -> Result { + if follow { + file.metadata() + } else { + file.symlink_metadata() + } +} + impl ChownExecutor { pub fn exec(&self) -> UResult<()> { let mut ret = 0; @@ -425,11 +425,9 @@ impl ChownExecutor { fn obtain_meta>(&self, path: P, follow: bool) -> Option { let path = path.as_ref(); - let meta = if follow { - path.metadata() - } else { - path.symlink_metadata() - }; + + let meta = get_metadata(path, follow); + match meta { Err(e) => { match self.verbosity.level { @@ -524,6 +522,45 @@ pub struct GidUidOwnerFilter { } type GidUidFilterOwnerParser = fn(&ArgMatches) -> UResult; +/// Determines symbolic link traversal and recursion settings based on flags. +/// Returns the updated `dereference` and `traverse_symlinks` values. +pub fn configure_symlink_and_recursion( + matches: &ArgMatches, +) -> Result<(bool, bool, TraverseSymlinks), Box> { + let mut dereference = if matches.get_flag(options::dereference::DEREFERENCE) { + Some(true) // Follow symlinks + } else if matches.get_flag(options::dereference::NO_DEREFERENCE) { + Some(false) // Do not follow symlinks + } else { + None // Default behavior + }; + + let mut traverse_symlinks = if matches.get_flag("L") { + TraverseSymlinks::All + } else if matches.get_flag("H") { + TraverseSymlinks::First + } else { + TraverseSymlinks::None + }; + + let recursive = matches.get_flag(options::RECURSIVE); + if recursive { + if traverse_symlinks == TraverseSymlinks::None { + if dereference == Some(true) { + return Err(USimpleError::new( + 1, + "-R --dereference requires -H or -L".to_string(), + )); + } + dereference = Some(false); + } + } else { + traverse_symlinks = TraverseSymlinks::None; + } + + Ok((recursive, dereference.unwrap_or(true), traverse_symlinks)) +} + /// Base implementation for `chgrp` and `chown`. /// /// An argument called `add_arg_if_not_reference` will be added to `command` if @@ -579,34 +616,7 @@ pub fn chown_base( .unwrap_or_default(); let preserve_root = matches.get_flag(options::preserve_root::PRESERVE); - - let mut dereference = if matches.get_flag(options::dereference::DEREFERENCE) { - Some(true) - } else if matches.get_flag(options::dereference::NO_DEREFERENCE) { - Some(false) - } else { - None - }; - - let mut traverse_symlinks = if matches.get_flag(options::traverse::TRAVERSE) { - TraverseSymlinks::First - } else if matches.get_flag(options::traverse::EVERY) { - TraverseSymlinks::All - } else { - TraverseSymlinks::None - }; - - let recursive = matches.get_flag(options::RECURSIVE); - if recursive { - if traverse_symlinks == TraverseSymlinks::None { - if dereference == Some(true) { - return Err(USimpleError::new(1, "-R --dereference requires -H or -L")); - } - dereference = Some(false); - } - } else { - traverse_symlinks = TraverseSymlinks::None; - } + let (recursive, dereference, traverse_symlinks) = configure_symlink_and_recursion(&matches)?; let verbosity_level = if matches.get_flag(options::verbosity::CHANGES) { VerbosityLevel::Changes @@ -636,7 +646,7 @@ pub fn chown_base( level: verbosity_level, }, recursive, - dereference: dereference.unwrap_or(true), + dereference, preserve_root, files, filter, @@ -644,6 +654,41 @@ pub fn chown_base( executor.exec() } +pub fn common_args() -> Vec { + vec![ + Arg::new(traverse::TRAVERSE) + .short(traverse::TRAVERSE.chars().next().unwrap()) + .help("if a command line argument is a symbolic link to a directory, traverse it") + .overrides_with_all([traverse::EVERY, traverse::NO_TRAVERSE]) + .action(clap::ArgAction::SetTrue), + Arg::new(traverse::EVERY) + .short(traverse::EVERY.chars().next().unwrap()) + .help("traverse every symbolic link to a directory encountered") + .overrides_with_all([traverse::TRAVERSE, traverse::NO_TRAVERSE]) + .action(clap::ArgAction::SetTrue), + Arg::new(traverse::NO_TRAVERSE) + .short(traverse::NO_TRAVERSE.chars().next().unwrap()) + .help("do not traverse any symbolic links (default)") + .overrides_with_all([traverse::TRAVERSE, traverse::EVERY]) + .action(clap::ArgAction::SetTrue), + Arg::new(options::dereference::DEREFERENCE) + .long(options::dereference::DEREFERENCE) + .help( + "affect the referent of each symbolic link (this is the default), \ + rather than the symbolic link itself", + ) + .action(clap::ArgAction::SetTrue), + Arg::new(options::dereference::NO_DEREFERENCE) + .short('h') + .long(options::dereference::NO_DEREFERENCE) + .help( + "affect symbolic links instead of any referenced file \ + (useful only on systems that can change the ownership of a symlink)", + ) + .action(clap::ArgAction::SetTrue), + ] +} + #[cfg(test)] mod tests { // Note this useful idiom: importing names from outer (for mod tests) scope. diff --git a/src/uucore/src/lib/features/proc_info.rs b/src/uucore/src/lib/features/proc_info.rs index 7c812ec2af9..f40847c1d69 100644 --- a/src/uucore/src/lib/features/proc_info.rs +++ b/src/uucore/src/lib/features/proc_info.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 exitstatus cmdline kworker pgrep pwait snice +// spell-checker:ignore exitstatus cmdline kworker pgrep pwait snice procps //! Set of functions to manage IDs //! @@ -19,6 +19,15 @@ //! `snice` (TBD) //! +// This file is currently flagged as dead code, because it isn't used anywhere +// in the codebase. It may be useful in the future though, so we decide to keep +// it. +// The code was originally written in procps +// (https://github.com/uutils/procps/blob/main/src/uu/pgrep/src/process.rs) +// but was eventually moved here. +// See https://github.com/uutils/coreutils/pull/6932 for discussion. +#![allow(dead_code)] + use crate::features::tty::Teletype; use std::hash::Hash; use std::{ diff --git a/src/uucore/src/lib/features/quoting_style.rs b/src/uucore/src/lib/features/quoting_style.rs index 1efa6f746b7..6d0265dc625 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: buf, + idx: 2, + state: EscapeOctalState::FirstBackslash, + } + } + + fn from_byte(b: u8) -> Self { Self { - c, + c: [0, b], idx: 2, - state: EscapeOctalState::Backslash, + 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) { - let mut must_quote = false; - let mut escaped_str = String::with_capacity(name.len()); +/// 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]) +} - for c in name.chars() { - let escaped = { - let ec = EscapedChar::new_shell(c, false, quotes); - if show_control_chars { - ec - } else { - ec.hide_control() - } - }; +fn shell_without_escape(name: &[u8], quotes: Quotes, show_control_chars: bool) -> (Vec, bool) { + let mut must_quote = false; + let mut escaped_str = Vec::with_capacity(name.len()); + let mut utf8_buf = vec![0; 4]; + + 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("''"); - in_dollar = false; + 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); } - escaped_str.push(x); - } - EscapeState::ForceQuote(x) => { - if in_dollar { - escaped_str.push_str("''"); - in_dollar = false; + EscapeState::ForceQuote(x) => { + if in_dollar { + escaped_str.push_str("''"); + in_dollar = false; + } + must_quote = true; + escaped_str.push(x); } - 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; + // 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("'\\''"); } - 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 222be7ca3a3..88851b9aae9 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 086c6ca9d03..df9e1673d9d 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 6142e688d7c..9516b5e1bf6 100644 --- a/src/uucore/src/lib/lib.rs +++ b/src/uucore/src/lib/lib.rs @@ -40,6 +40,8 @@ pub use crate::parser::shortcut_value_parser; // * feature-gated modules #[cfg(feature = "backup-control")] pub use crate::features::backup_control; +#[cfg(feature = "buf-copy")] +pub use crate::features::buf_copy; #[cfg(feature = "checksum")] pub use crate::features::checksum; #[cfg(feature = "colors")] @@ -74,7 +76,7 @@ pub use crate::features::mode; 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. +/// +/// 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. /// -/// 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 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); @@ -361,7 +380,10 @@ macro_rules! prompt_yes( eprint!("{}: ", uucore::util_name()); eprint!($($args)+); eprint!(" "); - uucore::crash_if_err!(1, std::io::stderr().flush()); + let res = std::io::stderr().flush().map_err(|err| { + $crate::error::USimpleError::new(1, err.to_string()) + }); + uucore::show_if_err!(res); uucore::read_yes() }) ); diff --git a/src/uucore/src/lib/macros.rs b/src/uucore/src/lib/macros.rs index 6fe60053886..3ef16ab4d5a 100644 --- a/src/uucore/src/lib/macros.rs +++ b/src/uucore/src/lib/macros.rs @@ -20,7 +20,7 @@ //! fully qualified name like this: //! //! ```no_run -//! use uucore::{show, crash}; +//! use uucore::show; //! ``` //! //! Here's an overview of the macros sorted by purpose @@ -30,8 +30,6 @@ //! [`crate::show_if_err!`] //! - From custom messages: [`crate::show_error!`] //! - Print warnings: [`crate::show_warning!`] -//! - Terminate util execution -//! - Crash program: [`crate::crash!`], [`crate::crash_if_err!`] // spell-checker:ignore sourcepath targetpath rustdoc @@ -189,57 +187,3 @@ macro_rules! show_warning_caps( eprintln!($($args)+); }) ); - -/// Display an error and [`std::process::exit`] -/// -/// Displays the provided error message using [`show_error!`], then invokes -/// [`std::process::exit`] with the provided exit code. -/// -/// # Examples -/// -/// ```should_panic -/// # #[macro_use] -/// # extern crate uucore; -/// # fn main() { -/// // outputs : Couldn't apply foo to bar -/// // and terminates execution -/// crash!(1, "Couldn't apply {} to {}", "foo", "bar"); -/// # } -/// ``` -#[macro_export] -macro_rules! crash( - ($exit_code:expr, $($args:tt)+) => ({ - $crate::show_error!($($args)+); - std::process::exit($exit_code); - }) -); - -/// Unwrap a [`std::result::Result`], crashing instead of panicking. -/// -/// If the result is an `Ok`-variant, returns the value contained inside. If it -/// is an `Err`-variant, invokes [`crash!`] with the formatted error instead. -/// -/// # Examples -/// -/// ```should_panic -/// # #[macro_use] -/// # extern crate uucore; -/// # fn main() { -/// let is_ok: Result = Ok(1); -/// // Does nothing -/// crash_if_err!(1, is_ok); -/// -/// let is_err: Result = Err("This didn't work..."); -/// // Calls `crash!` -/// crash_if_err!(1, is_err); -/// # } -/// ``` -#[macro_export] -macro_rules! crash_if_err( - ($exit_code:expr, $exp:expr) => ( - match $exp { - Ok(m) => m, - Err(f) => $crate::crash!($exit_code, "{}", f), - } - ) -); diff --git a/src/uucore_procs/Cargo.toml b/src/uucore_procs/Cargo.toml index 40ad6cd5b42..196544091d3 100644 --- a/src/uucore_procs/Cargo.toml +++ b/src/uucore_procs/Cargo.toml @@ -1,7 +1,7 @@ # spell-checker:ignore uuhelp [package] name = "uucore_procs" -version = "0.0.28" +version = "0.0.29" authors = ["Roy Ivy III "] license = "MIT" description = "uutils ~ 'uucore' proc-macros" @@ -19,4 +19,4 @@ proc-macro = true [dependencies] proc-macro2 = "1.0.81" quote = "1.0.36" -uuhelp_parser = { path = "../uuhelp_parser", version = "0.0.28" } +uuhelp_parser = { path = "../uuhelp_parser", version = "0.0.29" } diff --git a/src/uuhelp_parser/Cargo.toml b/src/uuhelp_parser/Cargo.toml index 1df8ae889c6..af46f719595 100644 --- a/src/uuhelp_parser/Cargo.toml +++ b/src/uuhelp_parser/Cargo.toml @@ -1,7 +1,7 @@ # spell-checker:ignore uuhelp [package] name = "uuhelp_parser" -version = "0.0.28" +version = "0.0.29" edition = "2021" license = "MIT" description = "A collection of functions to parse the markdown code of help files" diff --git a/src/uuhelp_parser/src/lib.rs b/src/uuhelp_parser/src/lib.rs index da50c037b72..0e0907f8a54 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 f07da925f5b..29b9edf0251 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 85c05ad3ee0..c0f40cd1d25 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_chmod.rs b/tests/by-util/test_chmod.rs index 167e79cd070..6f508afd6ce 100644 --- a/tests/by-util/test_chmod.rs +++ b/tests/by-util/test_chmod.rs @@ -229,19 +229,52 @@ fn test_chmod_ugoa() { }, ]; run_tests(tests); +} + +#[test] +#[cfg(any(target_os = "linux", target_os = "macos"))] +// TODO fix android, it has 0777 +// We should force for the umask on startup +fn test_chmod_umask_expected() { + let current_umask = uucore::mode::get_umask(); + assert_eq!( + current_umask, + 0o022, + "Unexpected umask value: expected 022 (octal), but got {:03o}. Please adjust the test environment.", + current_umask + ); +} +fn get_expected_symlink_permissions() -> u32 { + #[cfg(any(target_os = "linux", target_os = "android"))] + { + 0o120_777 + } + #[cfg(not(any(target_os = "linux", target_os = "android")))] + { + 0o120_755 + } +} + +#[test] +fn test_chmod_error_permissions() { // check that we print an error if umask prevents us from removing a permission let (at, mut ucmd) = at_and_ucmd!(); + at.touch("file"); set_permissions(at.plus("file"), Permissions::from_mode(0o777)).unwrap(); + ucmd.args(&["-w", "file"]) + .umask(0o022) .fails() .code_is(1) - // spell-checker:disable-next-line - .stderr_is("chmod: file: new permissions are r-xrwxrwx, not r-xr-xr-x\n"); + .stderr_is( + // spell-checker:disable-next-line + "chmod: file: new permissions are r-xrwxrwx, not r-xr-xr-x\n", + ); assert_eq!( metadata(at.plus("file")).unwrap().permissions().mode(), - 0o100577 + 0o100_577 ); } @@ -642,7 +675,10 @@ fn test_chmod_file_symlink_after_non_existing_file() { .stderr_contains(expected_stderr); assert_eq!( at.metadata(test_existing_symlink).permissions().mode(), - 0o100_764 + 0o100_764, + "Expected mode: {:o}, but got: {:o}", + 0o100_764, + at.metadata(test_existing_symlink).permissions().mode() ); } @@ -746,3 +782,192 @@ fn test_gnu_special_options() { scene.ucmd().arg("--").arg("--").arg("file").succeeds(); scene.ucmd().arg("--").arg("--").fails(); } + +#[test] +fn test_chmod_dereference_symlink() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let target = "file"; + let symlink = "symlink"; + + at.touch(target); + set_permissions(at.plus(target), Permissions::from_mode(0o664)).unwrap(); + at.symlink_file(target, symlink); + + // Use --dereference: should modify the target file's permissions + scene + .ucmd() + .arg("--dereference") + .arg("u+x") + .arg(symlink) + .succeeds() + .no_stderr(); + assert_eq!(at.metadata(target).permissions().mode(), 0o100_764); + assert_eq!( + at.symlink_metadata(symlink).permissions().mode(), + get_expected_symlink_permissions() + ); +} + +#[test] +fn test_chmod_no_dereference_symlink() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let target = "file"; + let symlink = "symlink"; + + at.touch(target); + set_permissions(at.plus(target), Permissions::from_mode(0o664)).unwrap(); + at.symlink_file(target, symlink); + + scene + .ucmd() + .arg("--no-dereference") + .arg("u+x") + .arg(symlink) + .succeeds() + .no_stderr(); + assert_eq!(at.metadata(target).permissions().mode(), 0o100_664); + assert_eq!( + at.symlink_metadata(symlink).permissions().mode(), + get_expected_symlink_permissions() + ); +} + +#[test] +fn test_chmod_symlink_to_dangling_target_dereference() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let dangling_target = "nonexistent_file"; + let symlink = "symlink"; + + at.symlink_file(dangling_target, symlink); + + // Use --dereference: should fail due to dangling symlink + scene + .ucmd() + .arg("--dereference") + .arg("u+x") + .arg(symlink) + .fails() + .stderr_contains(format!("cannot operate on dangling symlink '{}'", symlink)); +} + +#[test] +fn test_chmod_symlink_target_no_dereference() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let file = "a"; + let symlink = "symlink"; + at.touch(file); + at.symlink_file(file, symlink); + set_permissions(at.plus(file), Permissions::from_mode(0o644)).unwrap(); + + scene + .ucmd() + .arg("--no-dereference") + .arg("755") + .arg(symlink) + .succeeds() + .no_stderr(); + assert_eq!( + at.symlink_metadata(file).permissions().mode(), + 0o100_644, + "Expected symlink permissions: {:o}, but got: {:o}", + 0o100_644, + at.symlink_metadata(file).permissions().mode() + ); +} + +#[test] +fn test_chmod_symlink_to_dangling_recursive() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let dangling_target = "nonexistent_file"; + let symlink = "symlink"; + + at.symlink_file(dangling_target, symlink); + + scene + .ucmd() + .arg("755") + .arg("-R") + .arg(symlink) + .fails() + .stderr_is("chmod: cannot operate on dangling symlink 'symlink'\n"); + assert_eq!( + at.symlink_metadata(symlink).permissions().mode(), + get_expected_symlink_permissions(), + "Expected symlink permissions: {:o}, but got: {:o}", + get_expected_symlink_permissions(), + at.symlink_metadata(symlink).permissions().mode() + ); +} + +#[test] +fn test_chmod_traverse_symlink_combo() { + let scenarios = [ + ( + vec!["-R", "-H"], + 0o100_664, + get_expected_symlink_permissions(), + ), + ( + vec!["-R", "-L"], + 0o100_764, + get_expected_symlink_permissions(), + ), + ( + vec!["-R", "-P"], + 0o100_664, + get_expected_symlink_permissions(), + ), + ]; + + for (flags, expected_target_perms, expected_symlink_perms) in scenarios { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let directory = "dir"; + let target = "file"; + let symlink = "symlink"; + + at.mkdir(directory); + at.touch(target); + at.symlink_file(target, &format!("{directory}/{symlink}")); + + set_permissions(at.plus(target), Permissions::from_mode(0o664)).unwrap(); + + let mut ucmd = scene.ucmd(); + for f in &flags { + ucmd.arg(f); + } + ucmd.arg("u+x") + .umask(0o022) + .arg(directory) + .succeeds() + .no_stderr(); + + let actual_target = at.metadata(target).permissions().mode(); + assert_eq!( + actual_target, expected_target_perms, + "For flags {:?}, expected target perms = {:o}, got = {:o}", + flags, expected_target_perms, actual_target + ); + + let actual_symlink = at + .symlink_metadata(&format!("{directory}/{symlink}")) + .permissions() + .mode(); + assert_eq!( + actual_symlink, expected_symlink_perms, + "For flags {:?}, expected symlink perms = {:o}, got = {:o}", + flags, expected_symlink_perms, actual_symlink + ); + } +} diff --git a/tests/by-util/test_chroot.rs b/tests/by-util/test_chroot.rs index bf6b2ce16f1..022822c6b36 100644 --- a/tests/by-util/test_chroot.rs +++ b/tests/by-util/test_chroot.rs @@ -53,15 +53,73 @@ fn test_no_such_directory() { .code_is(125); } +#[test] +fn test_multiple_group_args() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + at.mkdir("id"); + + if let Ok(result) = run_ucmd_as_root( + &ts, + &["--groups='invalid ignored'", "--groups=''", "/", "id", "-G"], + ) { + result.success().stdout_is("0"); + } else { + print!("Test skipped; requires root user"); + } +} + #[test] fn test_invalid_user_spec() { - let (at, mut ucmd) = at_and_ucmd!(); + let ts = TestScenario::new(util_name!()); - at.mkdir("a"); + if let Ok(result) = run_ucmd_as_root(&ts, &["--userspec=ARABA:", "/"]) { + result + .failure() + .code_is(125) + .stderr_is("chroot: invalid user"); + } else { + print!("Test skipped; requires root user"); + } - let result = ucmd.arg("a").arg("--userspec=ARABA:").fails(); - result.code_is(125); - assert!(result.stderr_str().starts_with("chroot: invalid userspec")); + if let Ok(result) = run_ucmd_as_root(&ts, &["--userspec=ARABA:ARABA", "/"]) { + result + .failure() + .code_is(125) + .stderr_is("chroot: invalid user"); + } else { + print!("Test skipped; requires root user"); + } + + if let Ok(result) = run_ucmd_as_root(&ts, &["--userspec=:ARABA", "/"]) { + result + .failure() + .code_is(125) + .stderr_is("chroot: invalid group"); + } else { + print!("Test skipped; requires root user"); + } +} + +#[test] +fn test_invalid_user() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + + let dir = "CHROOT_DIR"; + at.mkdir(dir); + if let Ok(result) = run_ucmd_as_root(&ts, &[dir, "whoami"]) { + result.success().no_stderr().stdout_is("root"); + } else { + print!("Test skipped; requires root user"); + } + + // `--user` is an abbreviation of `--userspec`. + if let Ok(result) = run_ucmd_as_root(&ts, &["--user=nobody:+65535", dir, "pwd"]) { + result.failure().stderr_is("chroot: invalid user"); + } else { + print!("Test skipped; requires root user"); + } } #[test] @@ -94,11 +152,12 @@ fn test_preference_of_userspec() { at.mkdir("a"); + // `--user` is an abbreviation of `--userspec`. let result = ucmd .arg("a") .arg("--user") .arg("fake") - .arg("-G") + .arg("--groups") .arg("ABC,DEF") .arg(format!("--userspec={username}:{group_name}")) .fails(); diff --git a/tests/by-util/test_cksum.rs b/tests/by-util/test_cksum.rs index 98366cbec5e..b7c11320e11 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; @@ -352,6 +352,18 @@ fn test_length_not_supported() { .no_stdout() .stderr_contains("--length is only supported with --algorithm=blake2b") .code_is(1); + + new_ucmd!() + .arg("-l") + .arg("158") + .arg("-c") + .arg("-a") + .arg("crc") + .arg("/tmp/xxx") + .fails() + .no_stdout() + .stderr_contains("--length is only supported with --algorithm=blake2b") + .code_is(1); } #[test] @@ -736,6 +748,19 @@ fn test_conflicting_options() { "cksum: the --binary and --text options are meaningless when verifying checksums", ) .code_is(1); + + scene + .ucmd() + .arg("--tag") + .arg("-c") + .arg("-a") + .arg("md5") + .fails() + .no_stdout() + .stderr_contains( + "cksum: the --binary and --text options are meaningless when verifying checksums", + ) + .code_is(1); } #[test] @@ -1251,33 +1276,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 +1428,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 +1457,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 +1504,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 2dc385ef3f2..bad00b1290e 100644 --- a/tests/by-util/test_comm.rs +++ b/tests/by-util/test_comm.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) defaultcheck nocheck +// spell-checker:ignore (words) defaultcheck nocheck helpb helpz nwordb nwordwordz wordtotal use crate::common::util::TestScenario; @@ -13,111 +13,184 @@ fn test_invalid_arg() { #[test] fn ab_no_args() { - new_ucmd!() + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("a", "a\nz\n"); + at.write("b", "b\nz\n"); + + scene + .ucmd() .args(&["a", "b"]) .succeeds() - .stdout_only_fixture("ab.expected"); + .stdout_is("a\n\tb\n\t\tz\n"); } #[test] fn ab_dash_one() { - new_ucmd!() + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("a", "a\nz\n"); + at.write("b", "b\nz\n"); + + scene + .ucmd() .args(&["a", "b", "-1"]) .succeeds() - .stdout_only_fixture("ab1.expected"); + .stdout_is("b\n\tz\n"); } #[test] fn ab_dash_two() { - new_ucmd!() + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("a", "a\nz\n"); + at.write("b", "b\nz\n"); + + scene + .ucmd() .args(&["a", "b", "-2"]) .succeeds() - .stdout_only_fixture("ab2.expected"); + .stdout_is("a\n\tz\n"); } #[test] fn ab_dash_three() { - new_ucmd!() + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("a", "a\nz\n"); + at.write("b", "b\nz\n"); + + scene + .ucmd() .args(&["a", "b", "-3"]) .succeeds() - .stdout_only_fixture("ab3.expected"); + .stdout_is("a\n\tb\n"); } #[test] fn a_empty() { - new_ucmd!() + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("a", "a\nz\n"); + at.touch("empty"); + scene + .ucmd() .args(&["a", "empty"]) .succeeds() - .stdout_only_fixture("aempty.expected"); // spell-checker:disable-line + .stdout_is("a\nz\n"); } #[test] fn empty_empty() { - new_ucmd!() + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.touch("empty"); + scene + .ucmd() .args(&["empty", "empty"]) .succeeds() - .stdout_only_fixture("emptyempty.expected"); // spell-checker:disable-line + .no_output(); } #[test] fn total() { - new_ucmd!() + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("a", "a\nz\n"); + at.write("b", "b\nz\n"); + scene + .ucmd() .args(&["--total", "a", "b"]) .succeeds() - .stdout_is_fixture("ab_total.expected"); + .stdout_is("a\n\tb\n\t\tz\n1\t1\t1\ttotal\n"); } #[test] fn total_with_suppressed_regular_output() { - new_ucmd!() + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("a", "a\nz\n"); + at.write("b", "b\nz\n"); + scene + .ucmd() .args(&["--total", "-123", "a", "b"]) .succeeds() - .stdout_is_fixture("ab_total_suppressed_regular_output.expected"); + .stdout_is("1\t1\t1\ttotal\n"); } #[test] fn repeated_flags() { - new_ucmd!() + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("a", "a\nz\n"); + at.write("b", "b\nz\n"); + scene + .ucmd() .args(&["--total", "-123123", "--total", "a", "b"]) .succeeds() - .stdout_is_fixture("ab_total_suppressed_regular_output.expected"); + .stdout_is("1\t1\t1\ttotal\n"); } #[test] fn total_with_output_delimiter() { - new_ucmd!() + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("a", "a\nz\n"); + at.write("b", "b\nz\n"); + scene + .ucmd() .args(&["--total", "--output-delimiter=word", "a", "b"]) .succeeds() - .stdout_is_fixture("ab_total_delimiter_word.expected"); + .stdout_is("a\nwordb\nwordwordz\n1word1word1wordtotal\n"); } #[test] fn output_delimiter() { - new_ucmd!() + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("a", "a\nz\n"); + at.write("b", "b\nz\n"); + scene + .ucmd() .args(&["--output-delimiter=word", "a", "b"]) .succeeds() - .stdout_only_fixture("ab_delimiter_word.expected"); + .stdout_is("a\nwordb\nwordwordz\n"); } #[test] fn output_delimiter_hyphen_one() { - new_ucmd!() + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("a", "a\nz\n"); + at.write("b", "b\nz\n"); + scene + .ucmd() .args(&["--output-delimiter", "-1", "a", "b"]) .succeeds() - .stdout_only_fixture("ab_delimiter_hyphen_one.expected"); + .stdout_is("a\n-1b\n-1-1z\n"); } #[test] fn output_delimiter_hyphen_help() { - new_ucmd!() + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("a", "a\nz\n"); + at.write("b", "b\nz\n"); + scene + .ucmd() .args(&["--output-delimiter", "--help", "a", "b"]) .succeeds() - .stdout_only_fixture("ab_delimiter_hyphen_help.expected"); + .stdout_is("a\n--helpb\n--help--helpz\n"); } #[test] fn output_delimiter_multiple_identical() { - new_ucmd!() + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("a", "a\nz\n"); + at.write("b", "b\nz\n"); + scene + .ucmd() .args(&[ "--output-delimiter=word", "--output-delimiter=word", @@ -125,12 +198,17 @@ fn output_delimiter_multiple_identical() { "b", ]) .succeeds() - .stdout_only_fixture("ab_delimiter_word.expected"); + .stdout_is("a\nwordb\nwordwordz\n"); } #[test] fn output_delimiter_multiple_different() { - new_ucmd!() + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("a", "a\nz\n"); + at.write("b", "b\nz\n"); + scene + .ucmd() .args(&[ "--output-delimiter=word", "--output-delimiter=other", @@ -147,7 +225,12 @@ fn output_delimiter_multiple_different() { #[test] #[ignore = "This is too weird; deviate intentionally."] fn output_delimiter_multiple_different_prevents_help() { - new_ucmd!() + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("a", "a\nz\n"); + at.write("b", "b\nz\n"); + scene + .ucmd() .args(&[ "--output-delimiter=word", "--output-delimiter=other", @@ -164,59 +247,92 @@ fn output_delimiter_multiple_different_prevents_help() { #[test] fn output_delimiter_nul() { - new_ucmd!() + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("a", "a\nz\n"); + at.write("b", "b\nz\n"); + scene + .ucmd() .args(&["--output-delimiter=", "a", "b"]) .succeeds() - .stdout_only_fixture("ab_delimiter_nul.expected"); + .stdout_is("a\n\0b\n\0\0z\n"); } #[test] fn zero_terminated() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.write("a_nul", "a\0z\0"); + at.write("b_nul", "b\0z\0"); for param in ["-z", "--zero-terminated"] { - new_ucmd!() + scene + .ucmd() .args(&[param, "a_nul", "b_nul"]) .succeeds() - .stdout_only_fixture("ab_nul.expected"); + .stdout_is("a\0\tb\0\t\tz\0"); } } #[test] fn zero_terminated_provided_multiple_times() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.write("a_nul", "a\0z\0"); + at.write("b_nul", "b\0z\0"); for param in ["-z", "--zero-terminated"] { - new_ucmd!() + scene + .ucmd() .args(&[param, param, param, "a_nul", "b_nul"]) .succeeds() - .stdout_only_fixture("ab_nul.expected"); + .stdout_is("a\0\tb\0\t\tz\0"); } } #[test] fn zero_terminated_with_total() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.write("a_nul", "a\0z\0"); + at.write("b_nul", "b\0z\0"); + for param in ["-z", "--zero-terminated"] { - new_ucmd!() + scene + .ucmd() .args(&[param, "--total", "a_nul", "b_nul"]) .succeeds() - .stdout_only_fixture("ab_nul_total.expected"); + .stdout_is("a\0\tb\0\t\tz\x001\t1\t1\ttotal\0"); } } #[cfg_attr(not(feature = "test_unimplemented"), ignore)] #[test] fn check_order() { - new_ucmd!() + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("bad_order_1", "e\nd\nb\na\n"); + at.write("bad_order_2", "e\nc\nb\na\n"); + scene + .ucmd() .args(&["--check-order", "bad_order_1", "bad_order_2"]) .fails() - .stdout_is_fixture("bad_order12.check_order.expected") + .stdout_is("\t\te") .stderr_is("error to be defined"); } #[cfg_attr(not(feature = "test_unimplemented"), ignore)] #[test] fn nocheck_order() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("bad_order_1", "e\nd\nb\na\n"); + at.write("bad_order_2", "e\nc\nb\na\n"); new_ucmd!() .args(&["--nocheck-order", "bad_order_1", "bad_order_2"]) .succeeds() - .stdout_only_fixture("bad_order12.nocheck_order.expected"); + .stdout_is("\t\te\n\tc\n\tb\n\ta\nd\nb\na\n"); } // when neither --check-order nor --no-check-order is provided, @@ -225,6 +341,9 @@ fn nocheck_order() { #[cfg_attr(not(feature = "test_unimplemented"), ignore)] #[test] fn defaultcheck_order() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("bad_order_1", "e\nd\nb\na\n"); new_ucmd!() .args(&["a", "bad_order_1"]) .fails() @@ -233,27 +352,26 @@ fn defaultcheck_order() { // * the first: if both files are not in order, the default behavior is the only // behavior that will provide an error message - // * the second: if two rows are paired but are out of order, // it won't matter if all rows in the two files are exactly the same. // This is specified in the documentation - #[test] fn defaultcheck_order_identical_bad_order_files() { - new_ucmd!() + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("bad_order_1", "e\nd\nb\na\n"); + scene + .ucmd() .args(&["bad_order_1", "bad_order_1"]) .succeeds() - .stdout_only_fixture("bad_order11.defaultcheck_order.expected"); -} - -#[cfg_attr(not(feature = "test_unimplemented"), ignore)] -#[test] -fn defaultcheck_order_two_different_bad_order_files() { - new_ucmd!() - .args(&["bad_order_1", "bad_order_2"]) + .stdout_is("\t\te\n\t\td\n\t\tb\n\t\ta\n"); + scene + .ucmd() + .arg("--check-order") + .args(&["bad_order_1", "bad_order_1"]) .fails() - .stdout_is_fixture("bad_order12.nocheck_order.expected") - .stderr_is("error to be defined"); + .stdout_is("\t\te\n") + .stderr_is("comm: file 1 is not in sorted order\n"); } // * the third: (it is not know whether this is a bug or not) @@ -261,18 +379,21 @@ fn defaultcheck_order_two_different_bad_order_files() { // where both lines are different and one or both file lines being // compared are out of order from the preceding line, // it is ignored and no errors occur. - // * the fourth: (it is not known whether this is a bug or not) // there are additional, not-yet-understood circumstances where an out-of-order // pair is ignored and is not counted against the 1 maximum out-of-order line. - -#[cfg_attr(not(feature = "test_unimplemented"), ignore)] #[test] fn unintuitive_default_behavior_1() { - new_ucmd!() + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("defaultcheck_unintuitive_1", "m\nh\nn\no\nc\np\n"); + at.write("defaultcheck_unintuitive_2", "m\nh\nn\no\np\n"); + // Here, GNU does not fail, but uutils does + scene + .ucmd() .args(&["defaultcheck_unintuitive_1", "defaultcheck_unintuitive_2"]) - .succeeds() - .stdout_only_fixture("defaultcheck_unintuitive.expected"); + .fails() + .stdout_is("\t\tm\n\t\th\n\t\tn\n\t\to\nc\n\t\tp\n"); } #[test] @@ -292,3 +413,165 @@ 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"); +} + +#[test] +fn test_sorted() { + let expected_stderr = + "comm: file 2 is not in sorted order\ncomm: input is not in sorted order\n"; + + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("comm1", "1\n3"); + at.write("comm2", "3\n2"); + scene + .ucmd() + .args(&["comm1", "comm2"]) + .fails() + .code_is(1) + .stdout_is("1\n\t\t3\n\t2\n") + .stderr_is(expected_stderr); +} + +#[test] +fn test_sorted_check_order() { + let expected_stderr = "comm: file 2 is not in sorted order\n"; + + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("comm1", "1\n3"); + at.write("comm2", "3\n2"); + scene + .ucmd() + .arg("--check-order") + .args(&["comm1", "comm2"]) + .fails() + .code_is(1) + .stdout_is("1\n\t\t3\n") + .stderr_is(expected_stderr); +} + +#[test] +fn test_both_inputs_out_of_order() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("file_a", "3\n1\n0\n"); + at.write("file_b", "3\n2\n0\n"); + + scene + .ucmd() + .args(&["file_a", "file_b"]) + .fails() + .code_is(1) + .stdout_is("\t\t3\n1\n0\n\t2\n\t0\n") + .stderr_is( + "comm: file 1 is not in sorted order\n\ + comm: file 2 is not in sorted order\n\ + comm: input is not in sorted order\n", + ); +} + +#[test] +fn test_both_inputs_out_of_order_last_pair() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("file_a", "3\n1\n"); + at.write("file_b", "3\n2\n"); + + scene + .ucmd() + .args(&["file_a", "file_b"]) + .fails() + .code_is(1) + .stdout_is("\t\t3\n1\n\t2\n") + .stderr_is( + "comm: file 1 is not in sorted order\n\ + comm: file 2 is not in sorted order\n\ + comm: input is not in sorted order\n", + ); +} + +#[test] +fn test_first_input_out_of_order_extended() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("file_a", "0\n3\n1\n"); + at.write("file_b", "2\n3\n"); + + scene + .ucmd() + .args(&["file_a", "file_b"]) + .fails() + .code_is(1) + .stdout_is("0\n\t2\n\t\t3\n1\n") + .stderr_is( + "comm: file 1 is not in sorted order\n\ + comm: input is not in sorted order\n", + ); +} + +#[test] +fn test_out_of_order_input_nocheck() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + // Create input files + at.write("file_a", "1\n3\n"); + at.write("file_b", "3\n2\n"); + + scene + .ucmd() + .arg("--nocheck-order") + .args(&["file_a", "file_b"]) + .succeeds() + .stdout_is("1\n\t\t3\n\t2\n") + .no_stderr(); +} + +#[test] +fn test_both_inputs_out_of_order_but_identical() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.write("file_a", "2\n1\n0\n"); + at.write("file_b", "2\n1\n0\n"); + + scene + .ucmd() + .args(&["file_a", "file_b"]) + .succeeds() + .stdout_is("\t\t2\n\t\t1\n\t\t0\n") + .no_stderr(); +} diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index 156daec1f15..e44f35b8797 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -3,11 +3,12 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. // spell-checker:ignore (flags) reflink (fs) tmpfs (linux) rlimit Rlim NOFILE clob btrfs neve ROOTDIR USERDIR procfs outfile uufs xattrs -// spell-checker:ignore bdfl hlsl IRWXO IRWXG +// spell-checker:ignore bdfl hlsl IRWXO IRWXG getfattr 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); @@ -2421,6 +2422,17 @@ fn test_cp_reflink_bad() { .stderr_contains("error: invalid value 'bad' for '--reflink[=]'"); } +#[test] +fn test_cp_conflicting_update() { + new_ucmd!() + .arg("-b") + .arg("--update=none") + .arg("a") + .arg("b") + .fails() + .stderr_contains("--backup is mutually exclusive with -n or --update=none-fail"); +} + #[test] #[cfg(any(target_os = "linux", target_os = "android"))] fn test_cp_reflink_insufficient_permission() { @@ -2512,7 +2524,7 @@ fn test_cp_sparse_always_non_empty() { const BUFFER_SIZE: usize = 4096 * 16 + 3; let (at, mut ucmd) = at_and_ucmd!(); - let mut buf: [u8; BUFFER_SIZE] = [0; BUFFER_SIZE]; + let mut buf = vec![0; BUFFER_SIZE].into_boxed_slice(); let blocks_to_touch = [buf.len() / 3, 2 * (buf.len() / 3)]; for i in blocks_to_touch { @@ -2528,7 +2540,7 @@ fn test_cp_sparse_always_non_empty() { let touched_block_count = blocks_to_touch.len() as u64 * at.metadata("dst_file_sparse").blksize() / 512; - assert_eq!(at.read_bytes("dst_file_sparse"), buf); + assert_eq!(at.read_bytes("dst_file_sparse").into_boxed_slice(), buf); assert_eq!(at.metadata("dst_file_sparse").blocks(), touched_block_count); } @@ -3364,6 +3376,29 @@ fn test_copy_dir_preserve_permissions() { assert_metadata_eq!(metadata1, metadata2); } +/// cp should preserve attributes of subdirectories when copying recursively. +#[cfg(all(not(windows), not(target_os = "freebsd"), not(target_os = "openbsd")))] +#[test] +fn test_copy_dir_preserve_subdir_permissions() { + let (at, mut ucmd) = at_and_ucmd!(); + at.mkdir("a1"); + at.mkdir("a1/a2"); + // Use different permissions for a better test + at.set_mode("a1/a2", 0o0555); + at.set_mode("a1", 0o0777); + + ucmd.args(&["-p", "-r", "a1", "b1"]) + .succeeds() + .no_stderr() + .no_stdout(); + + // Make sure everything is preserved + assert!(at.dir_exists("b1")); + assert!(at.dir_exists("b1/a2")); + assert_metadata_eq!(at.metadata("a1"), at.metadata("b1")); + assert_metadata_eq!(at.metadata("a1/a2"), at.metadata("b1/a2")); +} + /// Test for preserving permissions when copying a directory, even in /// the face of an inaccessible file in that directory. #[cfg(all(not(windows), not(target_os = "freebsd"), not(target_os = "openbsd")))] @@ -3436,15 +3471,9 @@ fn test_same_file_force_backup() { } /// Test for copying the contents of a FIFO as opposed to the FIFO object itself. -#[cfg(all(unix, not(target_os = "freebsd"), not(target_os = "openbsd")))] +#[cfg(unix)] #[test] fn test_copy_contents_fifo() { - // TODO this test should work on FreeBSD, but the command was - // causing an error: - // - // cp: 'fifo' -> 'outfile': the source path is neither a regular file nor a symlink to a regular file - // - // the underlying `std::fs:copy` doesn't support copying fifo on freeBSD let scenario = TestScenario::new(util_name!()); let at = &scenario.fixtures; @@ -5615,7 +5644,7 @@ mod link_deref { // which could be problematic if we aim to preserve ownership or mode. For example, when // copying a directory, the destination directory could temporarily be setgid on some filesystems. // This temporary setgid status could grant access to other users who share the same group -// ownership as the newly created directory.To mitigate this issue, when creating a directory we +// ownership as the newly created directory. To mitigate this issue, when creating a directory we // disable these excessive permissions. #[test] #[cfg(unix)] @@ -5919,3 +5948,102 @@ fn test_cp_no_file() { .code_is(1) .stderr_contains("error: the following required arguments were not provided:"); } + +#[test] +#[cfg(all( + unix, + not(any(target_os = "android", target_os = "macos", target_os = "openbsd")) +))] +fn test_cp_preserve_xattr_readonly_source() { + use crate::common::util::compare_xattrs; + use std::process::Command; + + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let source_file = "a"; + let dest_file = "e"; + + at.touch(source_file); + + let xattr_key = "user.test"; + match Command::new("setfattr") + .args([ + "-n", + xattr_key, + "-v", + "value", + &at.plus_as_string(source_file), + ]) + .status() + .map(|status| status.code()) + { + Ok(Some(0)) => {} + Ok(_) => { + println!("test skipped: setfattr failed"); + return; + } + Err(e) => { + println!("test skipped: setfattr failed with {e}"); + return; + } + } + + let getfattr_output = Command::new("getfattr") + .args([&at.plus_as_string(source_file)]) + .output() + .expect("Failed to run `getfattr` on the destination file"); + + assert!( + getfattr_output.status.success(), + "getfattr did not run successfully: {}", + String::from_utf8_lossy(&getfattr_output.stderr) + ); + + let stdout = String::from_utf8_lossy(&getfattr_output.stdout); + assert!( + stdout.contains(xattr_key), + "Expected '{}' not found in getfattr output:\n{}", + xattr_key, + stdout + ); + + at.set_readonly(source_file); + assert!(scene + .fixtures + .metadata(source_file) + .permissions() + .readonly()); + + scene + .ucmd() + .args(&[ + "--preserve=xattr", + &at.plus_as_string(source_file), + &at.plus_as_string(dest_file), + ]) + .succeeds() + .no_output(); + + assert!(scene.fixtures.metadata(dest_file).permissions().readonly()); + assert!( + compare_xattrs(&at.plus(source_file), &at.plus(dest_file)), + "Extended attributes were not preserved" + ); +} + +#[test] +#[cfg(unix)] +fn test_cp_from_stdin() { + let (at, mut ucmd) = at_and_ucmd!(); + let target = "target"; + let test_string = "Hello, World!\n"; + + ucmd.arg("/dev/fd/0") + .arg(target) + .pipe_in(test_string) + .succeeds(); + + assert!(at.file_exists(target)); + assert_eq!(at.read(target), test_string); +} diff --git a/tests/by-util/test_csplit.rs b/tests/by-util/test_csplit.rs index 03b8c92fc09..e062b6d551f 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] @@ -457,14 +469,14 @@ fn test_up_to_match_offset_option_suppress_matched() { let (at, mut ucmd) = at_and_ucmd!(); ucmd.args(&["numbers50.txt", "--suppress-matched", "/10/+4"]) .succeeds() - .stdout_only("27\n111\n"); + .stdout_only("30\n108\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, 10) + &generate(11, 14)); - assert_eq!(at.read("xx01"), generate(14, 51)); + assert_eq!(at.read("xx00"), generate(1, 14)); + assert_eq!(at.read("xx01"), generate(15, 51)); } #[test] @@ -472,14 +484,14 @@ fn test_up_to_match_negative_offset_option_suppress_matched() { let (at, mut ucmd) = at_and_ucmd!(); ucmd.args(&["numbers50.txt", "--suppress-matched", "/10/-4"]) .succeeds() - .stdout_only("10\n128\n"); + .stdout_only("10\n129\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, 6)); - assert_eq!(at.read("xx01"), generate(6, 10) + &generate(11, 51)); + assert_eq!(at.read("xx01"), generate(7, 51)); } #[test] @@ -1364,10 +1376,10 @@ fn zero_error() { #[test] fn no_such_file() { - let (_, mut ucmd) = at_and_ucmd!(); - ucmd.args(&["in", "0"]) + new_ucmd!() + .args(&["in", "0"]) .fails() - .stderr_contains("cannot access 'in': No such file or directory"); + .stderr_contains("cannot open 'in' for reading: No such file or directory"); } #[test] @@ -1405,3 +1417,52 @@ fn repeat_everything() { assert_eq!(at.read("xxz_004"), generate(37, 44 + 1)); assert_eq!(at.read("xxz_005"), generate(46, 50 + 1)); } + +#[cfg(unix)] +#[test] +fn test_named_pipe_input_file() { + let (at, mut ucmd) = at_and_ucmd!(); + + let mut fifo_writer = + create_named_pipe_with_writer(&at.plus_as_string("fifo"), &generate(1, 51)); + + let result = ucmd.args(&["fifo", "10"]).succeeds(); + fifo_writer.kill().unwrap(); + fifo_writer.wait().unwrap(); + result.stdout_only("18\n123\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, 10)); + assert_eq!(at.read("xx01"), generate(10, 51)); +} + +#[cfg(unix)] +fn create_named_pipe_with_writer(path: &str, data: &str) -> std::process::Child { + // cSpell:ignore IRWXU + nix::unistd::mkfifo(path, nix::sys::stat::Mode::S_IRWXU).unwrap(); + std::process::Command::new("sh") + .arg("-c") + .arg(format!("printf '{}' > {path}", data)) + .spawn() + .unwrap() +} + +#[test] +fn test_directory_input_file() { + let (at, mut ucmd) = at_and_ucmd!(); + at.mkdir("test_directory"); + + #[cfg(unix)] + ucmd.args(&["test_directory", "1"]) + .fails() + .code_is(1) + .stderr_only("csplit: read error: Is a directory\n"); + #[cfg(windows)] + ucmd.args(&["test_directory", "1"]) + .fails() + .code_is(1) + .stderr_only("csplit: cannot open 'test_directory' for reading: Permission denied\n"); +} diff --git a/tests/by-util/test_cut.rs b/tests/by-util/test_cut.rs index 86d3ddf0f3d..dbd26abb287 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_date.rs b/tests/by-util/test_date.rs index 553414af853..ac16fe83145 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -144,11 +144,12 @@ fn test_date_utc() { #[test] fn test_date_utc_issue_6495() { new_ucmd!() + .env("TZ", "UTC0") .arg("-u") .arg("-d") .arg("@0") .succeeds() - .stdout_is("Thu Jan 1 00:00:00 1970\n"); + .stdout_is("Thu Jan 1 00:00:00 UTC 1970\n"); } #[test] @@ -423,16 +424,18 @@ fn test_invalid_date_string() { #[test] fn test_date_one_digit_date() { new_ucmd!() + .env("TZ", "UTC0") .arg("-d") .arg("2000-1-1") .succeeds() - .stdout_contains("Sat Jan 1 00:00:00 2000"); + .stdout_only("Sat Jan 1 00:00:00 UTC 2000\n"); new_ucmd!() + .env("TZ", "UTC0") .arg("-d") .arg("2000-1-4") .succeeds() - .stdout_contains("Tue Jan 4 00:00:00 2000"); + .stdout_only("Tue Jan 4 00:00:00 UTC 2000\n"); } #[test] @@ -464,6 +467,7 @@ fn test_date_parse_from_format() { #[test] fn test_date_from_stdin() { new_ucmd!() + .env("TZ", "UTC0") .arg("-f") .arg("-") .pipe_in( @@ -473,8 +477,17 @@ fn test_date_from_stdin() { ) .succeeds() .stdout_is( - "Mon Mar 27 08:30:00 2023\n\ - Sat Apr 1 12:00:00 2023\n\ - Sat Apr 15 18:30:00 2023\n", + "Mon Mar 27 08:30:00 UTC 2023\n\ + Sat Apr 1 12:00:00 UTC 2023\n\ + Sat Apr 15 18:30:00 UTC 2023\n", ); } + +#[test] +fn test_date_empty_tz() { + new_ucmd!() + .env("TZ", "") + .arg("+%Z") + .succeeds() + .stdout_only("UTC\n"); +} diff --git a/tests/by-util/test_dd.rs b/tests/by-util/test_dd.rs index e1e55054a6f..57a2933201e 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"); @@ -1726,7 +1728,26 @@ fn test_iflag_directory_fails_when_file_is_passed_via_std_in() { .args(&["iflag=directory", "count=0"]) .set_stdin(std::process::Stdio::from(File::open(filename).unwrap())) .fails() - .stderr_contains("standard input: not a directory"); + .stderr_only("dd: setting flags for 'standard input': Not a directory\n"); +} + +#[test] +#[cfg(any(target_os = "linux", target_os = "android"))] +fn test_iflag_directory_passes_when_dir_is_redirected() { + new_ucmd!() + .args(&["iflag=directory", "count=0"]) + .set_stdin(std::process::Stdio::from(File::open(".").unwrap())) + .succeeds(); +} + +#[test] +#[cfg(any(target_os = "linux", target_os = "android"))] +fn test_iflag_directory_fails_when_file_is_piped_via_std_in() { + new_ucmd!() + .arg("iflag=directory") + .pipe_in("") + .fails() + .stderr_only("dd: setting flags for 'standard input': Not a directory\n"); } #[test] diff --git a/tests/by-util/test_dircolors.rs b/tests/by-util/test_dircolors.rs index 06d490c4a02..ffabe2923df 100644 --- a/tests/by-util/test_dircolors.rs +++ b/tests/by-util/test_dircolors.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 overridable +// spell-checker:ignore overridable colorterm use crate::common::util::TestScenario; use dircolors::{guess_syntax, OutputFmt, StrUtils}; @@ -253,3 +253,14 @@ fn test_repeated() { new_ucmd!().arg(arg).arg(arg).succeeds().no_stderr(); } } + +#[test] +fn test_colorterm_empty_with_wildcard() { + new_ucmd!() + .env("COLORTERM", "") + .pipe_in("COLORTERM ?*\nowt 40;33\n") + .args(&["-b", "-"]) + .succeeds() + .stdout_is("LS_COLORS='';\nexport LS_COLORS\n") + .no_stderr(); +} diff --git a/tests/by-util/test_du.rs b/tests/by-util/test_du.rs index ef6179e02b3..ecbf58b117b 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 136500b4894..d4430d05655 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 @@ -383,3 +390,55 @@ fn slash_eight_off_by_one() { .succeeds() .stdout_only(r"\8"); } + +mod posixly_correct { + use super::*; + + #[test] + fn ignore_options() { + for arg in ["--help", "--version", "-E -n 'foo'", "-nE 'foo'"] { + new_ucmd!() + .env("POSIXLY_CORRECT", "1") + .arg(arg) + .succeeds() + .stdout_only(format!("{arg}\n")); + } + } + + #[test] + fn process_n_option() { + new_ucmd!() + .env("POSIXLY_CORRECT", "1") + .args(&["-n", "foo"]) + .succeeds() + .stdout_only("foo"); + + // ignore -E & process escapes + new_ucmd!() + .env("POSIXLY_CORRECT", "1") + .args(&["-n", "-E", "foo\\cbar"]) + .succeeds() + .stdout_only("foo"); + } + + #[test] + fn process_escapes() { + new_ucmd!() + .env("POSIXLY_CORRECT", "1") + .arg("foo\\n") + .succeeds() + .stdout_only("foo\n\n"); + + new_ucmd!() + .env("POSIXLY_CORRECT", "1") + .arg("foo\\tbar") + .succeeds() + .stdout_only("foo\tbar\n"); + + new_ucmd!() + .env("POSIXLY_CORRECT", "1") + .arg("foo\\ctbar") + .succeeds() + .stdout_only("foo"); + } +} diff --git a/tests/by-util/test_env.rs b/tests/by-util/test_env.rs index 208feab6db2..79ca0d2f45c 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 { @@ -78,6 +80,15 @@ fn test_env_version() { .stdout_contains(util_name!()); } +#[test] +fn test_env_permissions() { + new_ucmd!() + .arg(".") + .fails() + .code_is(126) + .stderr_is("env: '.': Permission denied\n"); +} + #[test] fn test_echo() { #[cfg(target_os = "windows")] @@ -98,6 +109,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 +130,7 @@ fn test_debug_1() { ); } +#[cfg(feature = "echo")] #[test] fn test_debug_2() { let ts = TestScenario::new(util_name!()); @@ -144,6 +157,7 @@ fn test_debug_2() { ); } +#[cfg(feature = "echo")] #[test] fn test_debug1_part_of_string_arg() { let ts = TestScenario::new(util_name!()); @@ -165,6 +179,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 +666,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 +690,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 +738,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!()); @@ -876,6 +891,29 @@ fn test_env_arg_ignore_signal_empty() { .no_stderr() .stdout_contains("hello"); } + +#[test] +fn disallow_equals_sign_on_short_unset_option() { + let ts = TestScenario::new(util_name!()); + + ts.ucmd() + .arg("-u=") + .fails() + .code_is(125) + .stderr_contains("env: cannot unset '=': Invalid argument"); + ts.ucmd() + .arg("-u=A1B2C3") + .fails() + .code_is(125) + .stderr_contains("env: cannot unset '=A1B2C3': Invalid argument"); + ts.ucmd().arg("--split-string=A1B=2C3=").succeeds(); + ts.ucmd() + .arg("--unset=") + .fails() + .code_is(125) + .stderr_contains("env: cannot unset '': Invalid argument"); +} + #[cfg(test)] mod tests_split_iterator { diff --git a/tests/by-util/test_fmt.rs b/tests/by-util/test_fmt.rs index 9bb82ede562..fb641643058 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_head.rs b/tests/by-util/test_head.rs index 296d7dab7ca..6d7ecffb2df 100644 --- a/tests/by-util/test_head.rs +++ b/tests/by-util/test_head.rs @@ -157,6 +157,23 @@ fn test_negative_byte_syntax() { .stdout_is(""); } +#[test] +fn test_negative_bytes_greater_than_input_size_stdin() { + new_ucmd!() + .args(&["-c", "-2"]) + .pipe_in("a") + .succeeds() + .no_output(); +} + +#[test] +fn test_negative_bytes_greater_than_input_size_file() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + at.write_bytes("f", b"a"); + ts.ucmd().args(&["-c", "-2", "f"]).succeeds().no_output(); +} + #[test] fn test_negative_zero_lines() { new_ucmd!() @@ -458,3 +475,25 @@ fn test_all_but_last_lines() { .succeeds() .stdout_is_fixture("lorem_ipsum_backwards_15_lines.expected"); } + +#[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "netbsd"))] +#[test] +fn test_write_to_dev_full() { + use std::fs::OpenOptions; + + for append in [true, false] { + { + let dev_full = OpenOptions::new() + .write(true) + .append(append) + .open("/dev/full") + .unwrap(); + + new_ucmd!() + .pipe_in_fixture(INPUT) + .set_stdout(dev_full) + .run() + .stderr_contains("No space left on device"); + } + } +} diff --git a/tests/by-util/test_install.rs b/tests/by-util/test_install.rs index f1e3302e138..9c6e48c7b9d 100644 --- a/tests/by-util/test_install.rs +++ b/tests/by-util/test_install.rs @@ -1717,3 +1717,53 @@ fn test_install_root_combined() { run_and_check(&["-Cv", "c", "d"], "d", 0, 0); run_and_check(&["-Cv", "c", "d"], "d", 0, 0); } + +#[test] +#[cfg(unix)] +fn test_install_from_fifo() { + use std::fs::OpenOptions; + use std::io::Write; + use std::thread; + + let pipe_name = "pipe"; + let target_name = "target"; + let test_string = "Hello, world!\n"; + + let s = TestScenario::new(util_name!()); + s.fixtures.mkfifo(pipe_name); + assert!(s.fixtures.is_fifo(pipe_name)); + + let proc = s.ucmd().arg(pipe_name).arg(target_name).run_no_wait(); + + let pipe_path = s.fixtures.plus(pipe_name); + let thread = thread::spawn(move || { + let mut pipe = OpenOptions::new() + .write(true) + .create(false) + .open(pipe_path) + .unwrap(); + pipe.write_all(test_string.as_bytes()).unwrap(); + }); + + proc.wait().unwrap(); + thread.join().unwrap(); + + assert!(s.fixtures.file_exists(target_name)); + assert_eq!(s.fixtures.read(target_name), test_string); +} + +#[test] +#[cfg(unix)] +fn test_install_from_stdin() { + let (at, mut ucmd) = at_and_ucmd!(); + let target = "target"; + let test_string = "Hello, World!\n"; + + ucmd.arg("/dev/fd/0") + .arg(target) + .pipe_in(test_string) + .succeeds(); + + assert!(at.file_exists(target)); + assert_eq!(at.read(target), test_string); +} diff --git a/tests/by-util/test_kill.rs b/tests/by-util/test_kill.rs index e07def51379..ba2b963518d 100644 --- a/tests/by-util/test_kill.rs +++ b/tests/by-util/test_kill.rs @@ -296,3 +296,36 @@ fn test_kill_with_signal_exit_new_form() { .arg(format!("{}", target.pid())) .succeeds(); } + +#[test] +fn test_kill_with_signal_number_hidden_compatibility_option() { + let mut target = Target::new(); + new_ucmd!() + .arg("-n") + .arg("9") + .arg(format!("{}", target.pid())) + .succeeds(); + assert_eq!(target.wait_for_signal(), Some(9)); +} + +#[test] +fn test_kill_with_signal_and_list() { + let target = Target::new(); + new_ucmd!() + .arg("-s") + .arg("EXIT") + .arg(format!("{}", target.pid())) + .arg("-l") + .fails(); +} + +#[test] +fn test_kill_with_signal_and_table() { + let target = Target::new(); + new_ucmd!() + .arg("-s") + .arg("EXIT") + .arg(format!("{}", target.pid())) + .arg("-t") + .fails(); +} diff --git a/tests/by-util/test_ls.rs b/tests/by-util/test_ls.rs index 0c0d8e3a822..6ef7ac93a2e 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, @@ -1095,13 +1096,16 @@ fn test_ls_long() { let at = &scene.fixtures; at.touch(at.plus_as_string("test-long")); + #[cfg(not(windows))] + let regex = r"[-bcCdDlMnpPsStTx?]([r-][w-][xt-]){3}.*"; + #[cfg(windows)] + let regex = r"[-dl](r[w-]x){3}.*"; + + let re = &Regex::new(regex).unwrap(); + for arg in LONG_ARGS { let result = scene.ucmd().arg(arg).arg("test-long").succeeds(); - #[cfg(not(windows))] - result.stdout_matches(&Regex::new(r"[-bcCdDlMnpPsStTx?]([r-][w-][xt-]){3}.*").unwrap()); - - #[cfg(windows)] - result.stdout_matches(&Regex::new(r"[-dl](r[w-]x){3}.*").unwrap()); + result.stdout_matches(re); } } @@ -1114,23 +1118,30 @@ fn test_ls_long_format() { at.touch(at.plus_as_string("test-long-dir/test-long-file")); at.mkdir(at.plus_as_string("test-long-dir/test-long-dir")); - for arg in LONG_ARGS { - // Assuming sane username do not have spaces within them. - // A line of the output should be: - // One of the characters -bcCdDlMnpPsStTx? - // rwx, with - for missing permissions, thrice. - // Zero or one "." for indicating a file with security context - // A number, preceded by column whitespace, and followed by a single space. - // A username, currently [^ ], followed by column whitespace, twice (or thrice for Hurd). - // A number, followed by a single space. - // A month, followed by a single space. - // A day, preceded by column whitespace, and followed by a single space. - // Either a year or a time, currently [0-9:]+, preceded by column whitespace, - // and followed by a single space. - // Whatever comes after is irrelevant to this specific test. - scene.ucmd().arg(arg).arg("test-long-dir").succeeds().stdout_matches(&Regex::new( + // Assuming sane username do not have spaces within them. + // A line of the output should be: + // One of the characters -bcCdDlMnpPsStTx? + // rwx, with - for missing permissions, thrice. + // Zero or one "." for indicating a file with security context + // A number, preceded by column whitespace, and followed by a single space. + // A username, currently [^ ], followed by column whitespace, twice (or thrice for Hurd). + // A number, followed by a single space. + // A month, followed by a single space. + // A day, preceded by column whitespace, and followed by a single space. + // Either a year or a time, currently [0-9:]+, preceded by column whitespace, + // and followed by a single space. + // Whatever comes after is irrelevant to this specific test. + let re = &Regex::new( r"\n[-bcCdDlMnpPsStTx?]([r-][w-][xt-]){3}\.? +\d+ [^ ]+ +[^ ]+( +[^ ]+)? +\d+ [A-Z][a-z]{2} {0,2}\d{0,2} {0,2}[0-9:]+ " - ).unwrap()); + ).unwrap(); + + for arg in LONG_ARGS { + scene + .ucmd() + .arg(arg) + .arg("test-long-dir") + .succeeds() + .stdout_matches(re); } // This checks for the line with the .. entry. The uname and group should be digits. @@ -1329,10 +1340,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()), } @@ -2098,6 +2109,30 @@ fn test_ls_order_time() { } } +#[test] +fn test_ls_order_mtime() { + use std::time::SystemTime; + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let f3 = at.make_file("test-3"); + f3.set_modified(SystemTime::now()).unwrap(); + let f4 = at.make_file("test-4"); + f4.set_modified(SystemTime::now()).unwrap(); + let f1 = at.make_file("test-1"); + f1.set_modified(SystemTime::now()).unwrap(); + let f2 = at.make_file("test-2"); + f2.set_modified(SystemTime::now()).unwrap(); + + let result = scene.ucmd().arg("-t").arg("--time=mtime").succeeds(); + result.stdout_only("test-2\ntest-1\ntest-4\ntest-3\n"); + f3.set_modified(SystemTime::now()).unwrap(); + + f4.set_modified(SystemTime::now()).unwrap(); + let result = scene.ucmd().arg("-t").arg("--time=mtime").succeeds(); + result.stdout_only("test-4\ntest-3\ntest-2\ntest-1\n"); +} + #[test] fn test_ls_non_existing() { new_ucmd!().arg("doesntexist").fails(); @@ -5516,3 +5551,80 @@ 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 +} + +#[cfg(feature = "test_risky_names")] +#[test] +fn test_non_unicode_names() { + // more extensive unit tests for correct escaping etc. are in the quoting_style module + let scene = TestScenario::new(util_name!()); + let target_file = uucore::os_str_from_bytes(b"some-dir1/\xC0.file") + .expect("Only unix platforms can test non-unicode names"); + let target_dir = uucore::os_str_from_bytes(b"some-dir1/\xC0.dir") + .expect("Only unix platforms can test non-unicode names"); + let at = &scene.fixtures; + at.mkdir("some-dir1"); + at.touch(target_file); + at.mkdir(target_dir); + + scene + .ucmd() + .arg("--quoting-style=shell-escape") + .arg("some-dir1") + .succeeds() + .stdout_contains("''$'\\300''.dir'") + .stdout_contains("''$'\\300''.file'"); + + scene + .ucmd() + .arg("--quoting-style=literal") + .arg("--show-control-chars") + .arg("some-dir1") + .succeeds() + .stdout_is_bytes(b"\xC0.dir\n\xC0.file\n"); +} diff --git a/tests/by-util/test_mkfifo.rs b/tests/by-util/test_mkfifo.rs index 731b6c1d5cd..e25bbfc4494 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 d8bc49e8ef9..1419be4e940 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_numfmt.rs b/tests/by-util/test_numfmt.rs index 1ef588f26a9..0a7cdda0135 100644 --- a/tests/by-util/test_numfmt.rs +++ b/tests/by-util/test_numfmt.rs @@ -32,7 +32,7 @@ fn test_from_si() { new_ucmd!() .args(&["--from=si"]) .pipe_in("1000\n1.1M\n0.1G") - .run() + .succeeds() .stdout_is("1000\n1100000\n100000000\n"); } @@ -41,7 +41,7 @@ fn test_from_iec() { new_ucmd!() .args(&["--from=iec"]) .pipe_in("1024\n1.1M\n0.1G") - .run() + .succeeds() .stdout_is("1024\n1153434\n107374183\n"); } @@ -50,7 +50,7 @@ fn test_from_iec_i() { new_ucmd!() .args(&["--from=iec-i"]) .pipe_in("1.1Mi\n0.1Gi") - .run() + .succeeds() .stdout_is("1153434\n107374183\n"); } @@ -74,7 +74,7 @@ fn test_from_auto() { new_ucmd!() .args(&["--from=auto"]) .pipe_in("1K\n1Ki") - .run() + .succeeds() .stdout_is("1000\n1024\n"); } @@ -83,7 +83,7 @@ fn test_to_si() { new_ucmd!() .args(&["--to=si"]) .pipe_in("1000\n1100000\n100000000") - .run() + .succeeds() .stdout_is("1.0K\n1.1M\n100M\n"); } @@ -92,7 +92,7 @@ fn test_to_iec() { new_ucmd!() .args(&["--to=iec"]) .pipe_in("1024\n1153434\n107374182") - .run() + .succeeds() .stdout_is("1.0K\n1.2M\n103M\n"); } @@ -101,7 +101,7 @@ fn test_to_iec_i() { new_ucmd!() .args(&["--to=iec-i"]) .pipe_in("1024\n1153434\n107374182") - .run() + .succeeds() .stdout_is("1.0Ki\n1.2Mi\n103Mi\n"); } @@ -109,7 +109,7 @@ fn test_to_iec_i() { fn test_input_from_free_arguments() { new_ucmd!() .args(&["--from=si", "1K", "1.1M", "0.1G"]) - .run() + .succeeds() .stdout_is("1000\n1100000\n100000000\n"); } @@ -118,7 +118,7 @@ fn test_padding() { new_ucmd!() .args(&["--from=si", "--padding=8"]) .pipe_in("1K\n1.1M\n0.1G") - .run() + .succeeds() .stdout_is(" 1000\n 1100000\n100000000\n"); } @@ -127,7 +127,7 @@ fn test_negative_padding() { new_ucmd!() .args(&["--from=si", "--padding=-8"]) .pipe_in("1K\n1.1M\n0.1G") - .run() + .succeeds() .stdout_is("1000 \n1100000 \n100000000\n"); } @@ -136,7 +136,7 @@ fn test_header() { new_ucmd!() .args(&["--from=si", "--header=2"]) .pipe_in("header\nheader2\n1K\n1.1M\n0.1G") - .run() + .succeeds() .stdout_is("header\nheader2\n1000\n1100000\n100000000\n"); } @@ -145,7 +145,7 @@ fn test_header_default() { new_ucmd!() .args(&["--from=si", "--header"]) .pipe_in("header\n1K\n1.1M\n0.1G") - .run() + .succeeds() .stdout_is("header\n1000\n1100000\n100000000\n"); } @@ -153,7 +153,7 @@ fn test_header_default() { fn test_header_error_if_non_numeric() { new_ucmd!() .args(&["--header=two"]) - .run() + .fails() .stderr_is("numfmt: invalid header value 'two'\n"); } @@ -161,7 +161,7 @@ fn test_header_error_if_non_numeric() { fn test_header_error_if_0() { new_ucmd!() .args(&["--header=0"]) - .run() + .fails() .stderr_is("numfmt: invalid header value '0'\n"); } @@ -169,7 +169,7 @@ fn test_header_error_if_0() { fn test_header_error_if_negative() { new_ucmd!() .args(&["--header=-3"]) - .run() + .fails() .stderr_is("numfmt: invalid header value '-3'\n"); } @@ -178,25 +178,28 @@ fn test_negative() { new_ucmd!() .args(&["--from=si"]) .pipe_in("-1000\n-1.1M\n-0.1G") - .run() + .succeeds() .stdout_is("-1000\n-1100000\n-100000000\n"); new_ucmd!() .args(&["--to=iec-i"]) .pipe_in("-1024\n-1153434\n-107374182") - .run() + .succeeds() .stdout_is("-1.0Ki\n-1.2Mi\n-103Mi\n"); } #[test] fn test_negative_zero() { - new_ucmd!().pipe_in("-0\n-0.0").run().stdout_is("0\n0.0\n"); + new_ucmd!() + .pipe_in("-0\n-0.0") + .succeeds() + .stdout_is("0\n0.0\n"); } #[test] fn test_no_op() { new_ucmd!() .pipe_in("1024\n1234567") - .run() + .succeeds() .stdout_is("1024\n1234567\n"); } @@ -205,7 +208,7 @@ fn test_normalize() { new_ucmd!() .args(&["--from=si", "--to=si"]) .pipe_in("10000000K\n0.001K") - .run() + .succeeds() .stdout_is("10G\n1\n"); } @@ -213,7 +216,7 @@ fn test_normalize() { fn test_si_to_iec() { new_ucmd!() .args(&["--from=si", "--to=iec", "15334263563K"]) - .run() + .succeeds() .stdout_is("14T\n"); } @@ -222,7 +225,7 @@ fn test_should_report_invalid_empty_number_on_empty_stdin() { new_ucmd!() .args(&["--from=auto"]) .pipe_in("\n") - .run() + .fails() .stderr_is("numfmt: invalid number: ''\n"); } @@ -231,7 +234,7 @@ fn test_should_report_invalid_empty_number_on_blank_stdin() { new_ucmd!() .args(&["--from=auto"]) .pipe_in(" \t \n") - .run() + .fails() .stderr_is("numfmt: invalid number: ''\n"); } @@ -241,7 +244,7 @@ fn test_should_report_invalid_suffix_on_stdin() { new_ucmd!() .args(&["--from=auto"]) .pipe_in(format!("1{}", c as char)) - .run() + .fails() .stderr_is(format!( "numfmt: invalid suffix in input: '1{}'\n", c as char @@ -252,7 +255,7 @@ fn test_should_report_invalid_suffix_on_stdin() { new_ucmd!() .args(&["--from=auto"]) .pipe_in("NaN") - .run() + .fails() .stderr_is("numfmt: invalid suffix in input: 'NaN'\n"); } @@ -262,7 +265,7 @@ fn test_should_report_invalid_number_with_interior_junk() { new_ucmd!() .args(&["--from=auto"]) .pipe_in("1x0K") - .run() + .fails() .stderr_is("numfmt: invalid number: '1x0K'\n"); } @@ -271,14 +274,14 @@ fn test_should_skip_leading_space_from_stdin() { new_ucmd!() .args(&["--from=auto"]) .pipe_in(" 2Ki") - .run() + .succeeds() .stdout_is("2048\n"); // multi-line new_ucmd!() .args(&["--from=auto"]) .pipe_in("\t1Ki\n 2K") - .run() + .succeeds() .stdout_is("1024\n2000\n"); } @@ -287,7 +290,7 @@ fn test_should_convert_only_first_number_in_line() { new_ucmd!() .args(&["--from=auto"]) .pipe_in("1Ki 2M 3G") - .run() + .succeeds() .stdout_is("1024 2M 3G\n"); } @@ -296,13 +299,13 @@ fn test_leading_whitespace_should_imply_padding() { new_ucmd!() .args(&["--from=auto"]) .pipe_in(" 1K") - .run() + .succeeds() .stdout_is(" 1000\n"); new_ucmd!() .args(&["--from=auto"]) .pipe_in(" 202Ki") - .run() + .succeeds() .stdout_is(" 206848\n"); } @@ -311,7 +314,7 @@ fn test_should_calculate_implicit_padding_per_line() { new_ucmd!() .args(&["--from=auto"]) .pipe_in(" 1Ki\n 2K") - .run() + .succeeds() .stdout_is(" 1024\n 2000\n"); } @@ -319,7 +322,7 @@ fn test_should_calculate_implicit_padding_per_line() { fn test_leading_whitespace_in_free_argument_should_imply_padding() { new_ucmd!() .args(&["--from=auto", " 1Ki"]) - .run() + .succeeds() .stdout_is(" 1024\n"); } @@ -327,7 +330,7 @@ fn test_leading_whitespace_in_free_argument_should_imply_padding() { fn test_should_calculate_implicit_padding_per_free_argument() { new_ucmd!() .args(&["--from=auto", " 1Ki", " 2K"]) - .run() + .succeeds() .stdout_is(" 1024\n 2000\n"); } diff --git a/tests/by-util/test_printf.rs b/tests/by-util/test_printf.rs index 29a8cc9140f..9b29c404ca8 100644 --- a/tests/by-util/test_printf.rs +++ b/tests/by-util/test_printf.rs @@ -916,3 +916,50 @@ fn float_flag_position_space_padding() { .succeeds() .stdout_only(" +1.0"); } + +#[test] +fn float_abs_value_less_than_one() { + new_ucmd!() + .args(&["%g", "0.1171875"]) + .succeeds() + .stdout_only("0.117188"); + + // The original value from #7031 issue + new_ucmd!() + .args(&["%g", "-0.1171875"]) + .succeeds() + .stdout_only("-0.117188"); + + new_ucmd!() + .args(&["%g", "0.01171875"]) + .succeeds() + .stdout_only("0.0117188"); + + new_ucmd!() + .args(&["%g", "-0.01171875"]) + .succeeds() + .stdout_only("-0.0117188"); + + new_ucmd!() + .args(&["%g", "0.001171875001"]) + .succeeds() + .stdout_only("0.00117188"); + + new_ucmd!() + .args(&["%g", "-0.001171875001"]) + .succeeds() + .stdout_only("-0.00117188"); +} + +#[test] +fn float_switch_switch_decimal_scientific() { + new_ucmd!() + .args(&["%g", "0.0001"]) + .succeeds() + .stdout_only("0.0001"); + + new_ucmd!() + .args(&["%g", "0.00001"]) + .succeeds() + .stdout_only("1e-05"); +} diff --git a/tests/by-util/test_rm.rs b/tests/by-util/test_rm.rs index f997688c818..b220926fec1 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 a8bd1fb8359..d8ab71842a4 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] @@ -747,7 +698,7 @@ fn test_parse_error_hex() { new_ucmd!() .arg("0xlmnop") .fails() - .usage_error("invalid hexadecimal argument: '0xlmnop'"); + .usage_error("invalid floating point argument: '0xlmnop'"); } #[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] @@ -826,4 +787,131 @@ fn test_invalid_format() { .fails() .no_stdout() .stderr_contains("format '%g%g' has too many % directives"); + new_ucmd!() + .args(&["-f", "%g%", "1"]) + .fails() + .no_stdout() + .stderr_contains("format '%g%' has too many % directives"); + new_ucmd!() + .args(&["-f", "%", "1"]) + .fails() + .no_stdout() + .stderr_contains("format '%' ends in %"); +} + +#[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"); +} + +#[test] +fn test_parse_valid_hexadecimal_float_two_args() { + let test_cases = [ + (["0x1p-1", "2"], "0.5\n1.5\n"), + (["0x.8p16", "32768"], "32768\n"), + (["0xffff.4p-4", "4096"], "4095.95\n"), + (["0xA.A9p-1", "6"], "5.33008\n"), + (["0xa.a9p-1", "6"], "5.33008\n"), + (["0xffffffffffp-30", "1024"], "1024\n"), // spell-checker:disable-line + ]; + + for (input_arguments, expected_output) in &test_cases { + new_ucmd!() + .args(input_arguments) + .succeeds() + .stdout_only(expected_output); + } +} + +#[test] +fn test_parse_valid_hexadecimal_float_three_args() { + let test_cases = [ + (["0x3.4p-1", "0x4p-1", "4"], "1.625\n3.625\n"), + ( + ["-0x.ep-3", "-0x.1p-3", "-0x.fp-3"], + "-0.109375\n-0.117188\n", + ), + ]; + + for (input_arguments, expected_output) in &test_cases { + new_ucmd!() + .args(input_arguments) + .succeeds() + .stdout_only(expected_output); + } +} + +#[test] +fn test_parse_float_gnu_coreutils() { + // some values from GNU coreutils tests + new_ucmd!() + .args(&[".89999", "1e-7", ".8999901"]) + .succeeds() + .stdout_only("0.8999900\n0.8999901\n"); + + new_ucmd!() + .args(&["0", "0.000001", "0.000003"]) + .succeeds() + .stdout_only("0.000000\n0.000001\n0.000002\n0.000003\n"); +} + +#[test] +fn test_parse_out_of_bounds_exponents() { + // The value 1e-9223372036854775808 is used in GNU Coreutils and BigDecimal tests to verify + // overflows and undefined behavior. Let's test the value too. + new_ucmd!() + .args(&["1e-9223372036854775808"]) + .succeeds() + .stdout_only(""); +} + +#[ignore] +#[test] +fn test_parse_valid_hexadecimal_float_format_issues() { + // These tests detect differences in the representation of floating-point values with GNU seq. + // There are two key areas to investigate: + // + // 1. GNU seq uses long double (80-bit) types for values, while the current implementation + // relies on f64 (64-bit). This can lead to differences due to varying precision. However, it's + // likely not the primary cause, as even double (64-bit) values can differ when compared to + // f64. + // + // 2. GNU seq uses the %Lg format specifier for printing (see the "get_default_format" function + // ). It appears that Rust lacks a direct equivalent for this format. Additionally, %Lg + // can use %f (floating) or %e (scientific) depending on the precision. There also seem to be + // some differences in the behavior of C and Rust when displaying floating-point or scientific + // notation, at least without additional configuration. + // + // It makes sense to begin by experimenting with formats and attempting to replicate + // the printf("%Lg",...) behavior. Another area worth investigating is glibc, as reviewing its + // code may help uncover additional corner cases or test data that could reveal more issues. + + //Test output: 0.00000000992804416455328464508056640625 + new_ucmd!() + .args(&["0xa.a9p-30", "1"]) + .succeeds() + .stdout_only("9.92804e-09\n1\n"); } diff --git a/tests/by-util/test_sort.rs b/tests/by-util/test_sort.rs index 97bfc6a74d0..62aa07dae5d 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_split.rs b/tests/by-util/test_split.rs index e2390dba0ab..e6e91ccccc1 100644 --- a/tests/by-util/test_split.rs +++ b/tests/by-util/test_split.rs @@ -1973,3 +1973,20 @@ fn test_split_separator_same_multiple() { .args(&["-t:", "-t:", "-t,", "fivelines.txt"]) .fails(); } + +#[test] +fn test_long_lines() { + let (at, mut ucmd) = at_and_ucmd!(); + let line1 = format!("{:131070}\n", ""); + let line2 = format!("{:1}\n", ""); + let line3 = format!("{:131071}\n", ""); + let infile = [line1, line2, line3].concat(); + ucmd.args(&["-C", "131072"]) + .pipe_in(infile) + .succeeds() + .no_output(); + assert_eq!(at.read("xaa").len(), 131_071); + assert_eq!(at.read("xab").len(), 2); + assert_eq!(at.read("xac").len(), 131_072); + assert!(!at.plus("xad").exists()); +} diff --git a/tests/by-util/test_stat.rs b/tests/by-util/test_stat.rs index 8cb4493f0eb..cd74767283a 100644 --- a/tests/by-util/test_stat.rs +++ b/tests/by-util/test_stat.rs @@ -184,9 +184,74 @@ fn test_char() { ]; let ts = TestScenario::new(util_name!()); let expected_stdout = unwrap_or_return!(expected_result(&ts, &args)).stdout_move_str(); + eprintln!("{expected_stdout}"); ts.ucmd().args(&args).succeeds().stdout_is(expected_stdout); } +#[cfg(target_os = "linux")] +#[test] +fn test_printf_mtime_precision() { + // TODO Higher precision numbers (`%.3Y`, `%.4Y`, etc.) are + // formatted correctly, but we are not precise enough when we do + // some `mtime` computations, so we get `.7640` instead of + // `.7639`. This can be fixed by being more careful when + // transforming the number from `Metadata::mtime_nsec()` to the form + // used in rendering. + let args = ["-c", "%.0Y %.1Y %.2Y", "/dev/pts/ptmx"]; + let ts = TestScenario::new(util_name!()); + let expected_stdout = unwrap_or_return!(expected_result(&ts, &args)).stdout_move_str(); + eprintln!("{expected_stdout}"); + ts.ucmd().args(&args).succeeds().stdout_is(expected_stdout); +} + +#[cfg(feature = "touch")] +#[test] +fn test_timestamp_format() { + let ts = TestScenario::new(util_name!()); + + // Create a file with a specific timestamp for testing + ts.ccmd("touch") + .args(&["-d", "1970-01-01 18:43:33.023456789", "k"]) + .succeeds() + .no_stderr(); + + let test_cases = vec![ + // Basic timestamp formats + ("%Y", "67413"), + ("%.Y", "67413.023456789"), + ("%.1Y", "67413.0"), + ("%.3Y", "67413.023"), + ("%.6Y", "67413.023456"), + ("%.9Y", "67413.023456789"), + // Width and padding tests + ("%13.6Y", " 67413.023456"), + ("%013.6Y", "067413.023456"), + ("%-13.6Y", "67413.023456 "), + // Longer width/precision combinations + ("%18.10Y", " 67413.0234567890"), + ("%I18.10Y", " 67413.0234567890"), + ("%018.10Y", "0067413.0234567890"), + ("%-18.10Y", "67413.0234567890 "), + ]; + + for (format_str, expected) in test_cases { + let result = ts + .ucmd() + .args(&["-c", format_str, "k"]) + .succeeds() + .stdout_move_str(); + + assert_eq!( + result, + format!("{expected}\n"), + "Format '{}' failed.\nExpected: '{}'\nGot: '{}'", + format_str, + expected, + result, + ); + } +} + #[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))] #[test] fn test_date() { @@ -242,7 +307,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 +321,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 +339,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 +361,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 +400,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 +412,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_stdbuf.rs b/tests/by-util/test_stdbuf.rs index a86f893e084..4bee30fab14 100644 --- a/tests/by-util/test_stdbuf.rs +++ b/tests/by-util/test_stdbuf.rs @@ -10,6 +10,26 @@ fn invalid_input() { new_ucmd!().arg("-/").fails().code_is(125); } +#[test] +fn test_permission() { + new_ucmd!() + .arg("-o1") + .arg(".") + .fails() + .code_is(126) + .stderr_contains("Permission denied"); +} + +#[test] +fn test_no_such() { + new_ucmd!() + .arg("-o1") + .arg("no_such") + .fails() + .code_is(127) + .stderr_contains("No such file or directory"); +} + #[cfg(all(not(target_os = "windows"), not(target_os = "openbsd")))] #[test] fn test_stdbuf_unbuffered_stdout() { diff --git a/tests/by-util/test_tail.rs b/tests/by-util/test_tail.rs index 4c7c52c7c18..885e50ad3c0 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 c32759ed4c4..4f2437acea3 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 ebd7635e432..cd99f1c3adf 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/by-util/test_tsort.rs b/tests/by-util/test_tsort.rs index 49809e0dfc7..299a8f0bb50 100644 --- a/tests/by-util/test_tsort.rs +++ b/tests/by-util/test_tsort.rs @@ -75,3 +75,39 @@ fn test_error_on_dir() { .fails() .stderr_contains("tsort: tsort_test_dir: read error: Is a directory"); } + +#[test] +fn test_split_on_any_whitespace() { + new_ucmd!() + .pipe_in("a\nb\n") + .succeeds() + .stdout_only("a\nb\n"); +} + +#[test] +fn test_cycle() { + // The graph looks like: a --> b <==> c --> d + new_ucmd!() + .pipe_in("a b b c c d c b") + .fails() + .code_is(1) + .stdout_is("a\nc\nd\nb\n") + .stderr_is("tsort: -: input contains a loop:\ntsort: b\ntsort: c\n"); +} + +#[test] +fn test_two_cycles() { + // The graph looks like: + // + // a + // | + // V + // c <==> b <==> d + // + new_ucmd!() + .pipe_in("a b b c c b b d d b") + .fails() + .code_is(1) + .stdout_is("a\nc\nd\nb\n") + .stderr_is("tsort: -: input contains a loop:\ntsort: b\ntsort: c\ntsort: -: input contains a loop:\ntsort: b\ntsort: d\n"); +} diff --git a/tests/by-util/test_uniq.rs b/tests/by-util/test_uniq.rs index 30cf73b8436..18f226f07dd 100644 --- a/tests/by-util/test_uniq.rs +++ b/tests/by-util/test_uniq.rs @@ -1172,3 +1172,13 @@ fn gnu_tests() { } } } + +#[test] +fn test_stdin_w1_multibyte() { + let input = "à\ná\n"; + new_ucmd!() + .args(&["-w1"]) + .pipe_in(input) + .run() + .stdout_is("à\ná\n"); +} diff --git a/tests/by-util/test_wc.rs b/tests/by-util/test_wc.rs index 0bdb5c843a1..e2af757b360 100644 --- a/tests/by-util/test_wc.rs +++ b/tests/by-util/test_wc.rs @@ -283,6 +283,32 @@ fn test_gnu_compatible_quotation() { .stdout_is("0 0 0 'some-dir1/12'$'\\n''34.txt'\n"); } +#[cfg(feature = "test_risky_names")] +#[test] +fn test_non_unicode_names() { + let scene = TestScenario::new(util_name!()); + let target1 = uucore::os_str_from_bytes(b"some-dir1/1\xC0\n.txt") + .expect("Only unix platforms can test non-unicode names"); + let target2 = uucore::os_str_from_bytes(b"some-dir1/2\xC0\t.txt") + .expect("Only unix platforms can test non-unicode names"); + let at = &scene.fixtures; + at.mkdir("some-dir1"); + at.touch(&target1); + at.touch(&target2); + scene + .ucmd() + .args(&[target1, target2]) + .run() + .stdout_is_bytes( + [ + b"0 0 0 'some-dir1/1'$'\\300\\n''.txt'\n".to_vec(), + b"0 0 0 some-dir1/2\xC0\t.txt\n".to_vec(), + b"0 0 0 total\n".to_vec(), + ] + .concat(), + ); +} + #[test] fn test_multiple_default() { new_ucmd!() diff --git a/tests/common/util.rs b/tests/common/util.rs index 87c937492f3..844618def47 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/comm/a b/tests/fixtures/comm/a deleted file mode 100644 index 58f424cdb4f..00000000000 --- a/tests/fixtures/comm/a +++ /dev/null @@ -1,2 +0,0 @@ -a -z diff --git a/tests/fixtures/comm/a_nul b/tests/fixtures/comm/a_nul deleted file mode 100644 index 3142c801ccd..00000000000 Binary files a/tests/fixtures/comm/a_nul and /dev/null differ diff --git a/tests/fixtures/comm/ab.expected b/tests/fixtures/comm/ab.expected deleted file mode 100644 index 0efac0666b0..00000000000 --- a/tests/fixtures/comm/ab.expected +++ /dev/null @@ -1,3 +0,0 @@ -a - b - z diff --git a/tests/fixtures/comm/ab1.expected b/tests/fixtures/comm/ab1.expected deleted file mode 100644 index 7c81c8ba7cb..00000000000 --- a/tests/fixtures/comm/ab1.expected +++ /dev/null @@ -1,2 +0,0 @@ -b - z diff --git a/tests/fixtures/comm/ab2.expected b/tests/fixtures/comm/ab2.expected deleted file mode 100644 index c98cd0da66d..00000000000 --- a/tests/fixtures/comm/ab2.expected +++ /dev/null @@ -1,2 +0,0 @@ -a - z diff --git a/tests/fixtures/comm/ab3.expected b/tests/fixtures/comm/ab3.expected deleted file mode 100644 index 1898bc804f2..00000000000 --- a/tests/fixtures/comm/ab3.expected +++ /dev/null @@ -1,2 +0,0 @@ -a - b diff --git a/tests/fixtures/comm/ab_delimiter_hyphen_help.expected b/tests/fixtures/comm/ab_delimiter_hyphen_help.expected deleted file mode 100644 index c245aa27f13..00000000000 --- a/tests/fixtures/comm/ab_delimiter_hyphen_help.expected +++ /dev/null @@ -1,3 +0,0 @@ -a ---helpb ---help--helpz diff --git a/tests/fixtures/comm/ab_delimiter_hyphen_one.expected b/tests/fixtures/comm/ab_delimiter_hyphen_one.expected deleted file mode 100644 index 458f98dd42f..00000000000 --- a/tests/fixtures/comm/ab_delimiter_hyphen_one.expected +++ /dev/null @@ -1,3 +0,0 @@ -a --1b --1-1z diff --git a/tests/fixtures/comm/ab_delimiter_nul.expected b/tests/fixtures/comm/ab_delimiter_nul.expected deleted file mode 100644 index e56ec5cd789..00000000000 Binary files a/tests/fixtures/comm/ab_delimiter_nul.expected and /dev/null differ diff --git a/tests/fixtures/comm/ab_delimiter_word.expected b/tests/fixtures/comm/ab_delimiter_word.expected deleted file mode 100644 index a0c510e2071..00000000000 --- a/tests/fixtures/comm/ab_delimiter_word.expected +++ /dev/null @@ -1,3 +0,0 @@ -a -wordb -wordwordz diff --git a/tests/fixtures/comm/ab_nul.expected b/tests/fixtures/comm/ab_nul.expected deleted file mode 100644 index f826106bb19..00000000000 Binary files a/tests/fixtures/comm/ab_nul.expected and /dev/null differ diff --git a/tests/fixtures/comm/ab_nul_total.expected b/tests/fixtures/comm/ab_nul_total.expected deleted file mode 100644 index 9f46f9558b1..00000000000 Binary files a/tests/fixtures/comm/ab_nul_total.expected and /dev/null differ diff --git a/tests/fixtures/comm/ab_total.expected b/tests/fixtures/comm/ab_total.expected deleted file mode 100644 index 430354a6620..00000000000 --- a/tests/fixtures/comm/ab_total.expected +++ /dev/null @@ -1,4 +0,0 @@ -a - b - z -1 1 1 total diff --git a/tests/fixtures/comm/ab_total_delimiter_word.expected b/tests/fixtures/comm/ab_total_delimiter_word.expected deleted file mode 100644 index f7215ea28a3..00000000000 --- a/tests/fixtures/comm/ab_total_delimiter_word.expected +++ /dev/null @@ -1,4 +0,0 @@ -a -wordb -wordwordz -1word1word1wordtotal diff --git a/tests/fixtures/comm/ab_total_suppressed_regular_output.expected b/tests/fixtures/comm/ab_total_suppressed_regular_output.expected deleted file mode 100644 index 733c1d344e5..00000000000 --- a/tests/fixtures/comm/ab_total_suppressed_regular_output.expected +++ /dev/null @@ -1 +0,0 @@ -1 1 1 total diff --git a/tests/fixtures/comm/aempty.expected b/tests/fixtures/comm/aempty.expected deleted file mode 100644 index 58f424cdb4f..00000000000 --- a/tests/fixtures/comm/aempty.expected +++ /dev/null @@ -1,2 +0,0 @@ -a -z diff --git a/tests/fixtures/comm/b b/tests/fixtures/comm/b deleted file mode 100644 index 63d7d705891..00000000000 --- a/tests/fixtures/comm/b +++ /dev/null @@ -1,2 +0,0 @@ -b -z diff --git a/tests/fixtures/comm/b_nul b/tests/fixtures/comm/b_nul deleted file mode 100644 index ca4569669d4..00000000000 Binary files a/tests/fixtures/comm/b_nul and /dev/null differ diff --git a/tests/fixtures/comm/bad_order11.defaultcheck_order.expected b/tests/fixtures/comm/bad_order11.defaultcheck_order.expected deleted file mode 100644 index 81aa48629c9..00000000000 --- a/tests/fixtures/comm/bad_order11.defaultcheck_order.expected +++ /dev/null @@ -1,4 +0,0 @@ - e - d - b - a diff --git a/tests/fixtures/comm/bad_order12.check_order.expected b/tests/fixtures/comm/bad_order12.check_order.expected deleted file mode 100644 index 3930ff03699..00000000000 --- a/tests/fixtures/comm/bad_order12.check_order.expected +++ /dev/null @@ -1 +0,0 @@ - e diff --git a/tests/fixtures/comm/bad_order12.nocheck_order.expected b/tests/fixtures/comm/bad_order12.nocheck_order.expected deleted file mode 100644 index b5711801391..00000000000 --- a/tests/fixtures/comm/bad_order12.nocheck_order.expected +++ /dev/null @@ -1,7 +0,0 @@ - e - c - b - a -d -b -a diff --git a/tests/fixtures/comm/bad_order_1 b/tests/fixtures/comm/bad_order_1 deleted file mode 100644 index 2ebde65a6a1..00000000000 --- a/tests/fixtures/comm/bad_order_1 +++ /dev/null @@ -1,4 +0,0 @@ -e -d -b -a diff --git a/tests/fixtures/comm/bad_order_2 b/tests/fixtures/comm/bad_order_2 deleted file mode 100644 index 76603aeded1..00000000000 --- a/tests/fixtures/comm/bad_order_2 +++ /dev/null @@ -1,4 +0,0 @@ -e -c -b -a diff --git a/tests/fixtures/comm/defaultcheck_unintuitive.expected b/tests/fixtures/comm/defaultcheck_unintuitive.expected deleted file mode 100644 index 132eab5b250..00000000000 --- a/tests/fixtures/comm/defaultcheck_unintuitive.expected +++ /dev/null @@ -1,6 +0,0 @@ - m - h - n - o -c - p diff --git a/tests/fixtures/comm/defaultcheck_unintuitive_1 b/tests/fixtures/comm/defaultcheck_unintuitive_1 deleted file mode 100644 index f32fc1fd758..00000000000 --- a/tests/fixtures/comm/defaultcheck_unintuitive_1 +++ /dev/null @@ -1,6 +0,0 @@ -m -h -n -o -c -p diff --git a/tests/fixtures/comm/defaultcheck_unintuitive_2 b/tests/fixtures/comm/defaultcheck_unintuitive_2 deleted file mode 100644 index 212de88c4d0..00000000000 --- a/tests/fixtures/comm/defaultcheck_unintuitive_2 +++ /dev/null @@ -1,5 +0,0 @@ -m -h -n -o -p diff --git a/tests/fixtures/comm/empty b/tests/fixtures/comm/empty deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/fixtures/comm/emptyempty.expected b/tests/fixtures/comm/emptyempty.expected deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/fixtures/comm/lowercase_uppercase b/tests/fixtures/comm/lowercase_uppercase deleted file mode 100644 index 768ebc2b947..00000000000 --- a/tests/fixtures/comm/lowercase_uppercase +++ /dev/null @@ -1,2 +0,0 @@ -a -A \ No newline at end of file diff --git a/tests/fixtures/comm/lowercase_uppercase.c.expected b/tests/fixtures/comm/lowercase_uppercase.c.expected deleted file mode 100644 index 8458d7960bc..00000000000 --- a/tests/fixtures/comm/lowercase_uppercase.c.expected +++ /dev/null @@ -1 +0,0 @@ - a diff --git a/tests/fixtures/comm/lowercase_uppercase.en_us.expected b/tests/fixtures/comm/lowercase_uppercase.en_us.expected deleted file mode 100644 index 3992b34d039..00000000000 --- a/tests/fixtures/comm/lowercase_uppercase.en_us.expected +++ /dev/null @@ -1,2 +0,0 @@ - a - A diff --git a/tests/fixtures/dircolors/internal.expected b/tests/fixtures/dircolors/internal.expected index e151973f200..feea46455f4 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 b78db4af181..e317d4079ad 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 830e9afd091..41b7e210df8 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 60197b1deaa..0a3ea83034f 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 d3a56ffd6f6..c8e4ad9ae83 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 684187733de..782e21a1a30 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -2,7 +2,7 @@ # `build-gnu.bash` ~ builds GNU coreutils (from supplied sources) # -# spell-checker:ignore (paths) abmon deref discrim eacces getlimits getopt ginstall inacc infloop inotify reflink ; (misc) INT_OFLOW OFLOW baddecode submodules xstrtol ; (vars/env) SRCDIR vdir rcexp xpart dired OSTYPE ; (utils) gnproc greadlink gsed multihardlink +# spell-checker:ignore (paths) abmon deref discrim eacces getlimits getopt ginstall inacc infloop inotify reflink ; (misc) INT_OFLOW OFLOW baddecode submodules xstrtol ; (vars/env) SRCDIR vdir rcexp xpart dired OSTYPE ; (utils) gnproc greadlink gsed multihardlink texinfo set -e @@ -122,6 +122,8 @@ if test -f gnu-built; then echo "'rm -f $(pwd)/gnu-built' to force the build" echo "Note: the customization of the tests will still happen" else + # Disable useless checks + sed -i 's|check-texinfo: $(syntax_checks)|check-texinfo:|' doc/local.mk ./bootstrap --skip-po ./configure --quiet --disable-gcc-warnings --disable-nls --disable-dependency-tracking --disable-bold-man-page-references #Add timeout to to protect against hangs @@ -132,55 +134,51 @@ else # Use a better diff sed -i 's|diff -c|diff -u|g' tests/Coreutils.pm "${MAKE}" -j "$("${NPROC}")" - touch gnu-built -fi -# Handle generated factor tests -t_first=00 -t_max=36 -# t_max_release=20 -# if test "${UU_MAKE_PROFILE}" != "debug"; then -# # Generate the factor tests, so they can be fixed -# # * reduced to 20 to decrease log size (down from 36 expected by GNU) -# # * only for 'release', skipped for 'debug' as redundant and too time consuming (causing timeout errors) -# seq=$( -# i=${t_first} -# while test "${i}" -le "${t_max_release}"; do -# printf '%02d ' $i -# i=$((i + 1)) -# done -# ) -# for i in ${seq}; do -# "${MAKE}" "tests/factor/t${i}.sh" -# done -# cat -# sed -i -e 's|^seq |/usr/bin/seq |' -e 's|sha1sum |/usr/bin/sha1sum |' tests/factor/t*.sh -# t_first=$((t_max_release + 1)) -# fi -# strip all (debug) or just the longer (release) factor tests from Makefile -seq=$( - i=${t_first} - while test "${i}" -le "${t_max}"; do - printf '%02d ' ${i} - i=$((i + 1)) + # Handle generated factor tests + t_first=00 + t_max=36 + # t_max_release=20 + # if test "${UU_MAKE_PROFILE}" != "debug"; then + # # Generate the factor tests, so they can be fixed + # # * reduced to 20 to decrease log size (down from 36 expected by GNU) + # # * only for 'release', skipped for 'debug' as redundant and too time consuming (causing timeout errors) + # seq=$( + # i=${t_first} + # while test "${i}" -le "${t_max_release}"; do + # printf '%02d ' $i + # i=$((i + 1)) + # done + # ) + # for i in ${seq}; do + # "${MAKE}" "tests/factor/t${i}.sh" + # done + # cat + # sed -i -e 's|^seq |/usr/bin/seq |' -e 's|sha1sum |/usr/bin/sha1sum |' tests/factor/t*.sh + # t_first=$((t_max_release + 1)) + # fi + # strip all (debug) or just the longer (release) factor tests from Makefile + seq=$( + i=${t_first} + while test "${i}" -le "${t_max}"; do + printf '%02d ' ${i} + i=$((i + 1)) + done + ) + for i in ${seq}; do + echo "strip t${i}.sh from Makefile" + sed -i -e "s/\$(tf)\/t${i}.sh//g" Makefile done -) -for i in ${seq}; do - echo "strip t${i}.sh from Makefile" - sed -i -e "s/\$(tf)\/t${i}.sh//g" Makefile -done - -grep -rl 'path_prepend_' tests/* | xargs sed -i 's| path_prepend_ ./src||' -# Remove tests checking for --version & --help -# Not really interesting for us and logs are too big -sed -i -e '/tests\/help\/help-version.sh/ D' \ - -e '/tests\/help\/help-version-getopt.sh/ D' \ - Makefile + # Remove tests checking for --version & --help + # Not really interesting for us and logs are too big + sed -i -e '/tests\/help\/help-version.sh/ D' \ + -e '/tests\/help\/help-version-getopt.sh/ D' \ + Makefile + touch gnu-built +fi -# logs are clotted because of this test -sed -i -e '/tests\/seq\/seq-precision.sh/ D' \ - Makefile +grep -rl 'path_prepend_' tests/* | xargs sed -i 's| path_prepend_ ./src||' # printf doesn't limit the values used in its arg, so this produced ~2GB of output sed -i '/INT_OFLOW/ D' tests/printf/printf.sh @@ -204,6 +202,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 @@ -224,8 +225,6 @@ eval cat "$path_UUTILS/util/gnu-patches/*.patch" | patch -N -r - -d "$path_GNU" sed -i -e "s|rm: cannot remove 'e/slink'|rm: cannot remove 'e'|g" tests/rm/fail-eacces.sh -sed -i -e "s|rm: cannot remove 'a/b'|rm: cannot remove 'a'|g" tests/rm/fail-2eperm.sh - sed -i -e "s|rm: cannot remove 'a/b/file'|rm: cannot remove 'a'|g" tests/rm/cycle.sh sed -i -e "s|rm: cannot remove directory 'b/a/p'|rm: cannot remove 'b'|g" tests/rm/rm1.sh @@ -264,7 +263,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. @@ -301,7 +300,7 @@ sed -i -e "s/ginstall: creating directory/install: creating directory/g" tests/i "${SED}" -i -Ez "s/\n([^\n#]*pad-3\.2[^\n]*)\n([^\n]*)\n([^\n]*)/\n# uutils\/numfmt supports padding = LONG_MIN\n#\1\n#\2\n#\3/" tests/misc/numfmt.pl # Update the GNU error message to match the one generated by clap -sed -i -e "s/\$prog: multiple field specifications/error: The argument '--field ' was provided more than once, but cannot be used multiple times\n\nUsage: numfmt [OPTION]... [NUMBER]...\n\n\nFor more information try '--help'/g" tests/misc/numfmt.pl +sed -i -e "s/\$prog: multiple field specifications/error: the argument '--field ' cannot be used multiple times\n\nUsage: numfmt [OPTION]... [NUMBER]...\n\nFor more information, try '--help'./g" tests/misc/numfmt.pl sed -i -e "s/Try 'mv --help' for more information/For more information, try '--help'/g" -e "s/mv: missing file operand/error: the following required arguments were not provided:\n ...\n\nUsage: mv [OPTION]... [-T] SOURCE DEST\n mv [OPTION]... SOURCE... DIRECTORY\n mv [OPTION]... -t DIRECTORY SOURCE...\n/g" -e "s/mv: missing destination file operand after 'no-file'/error: The argument '...' requires at least 2 values, but only 1 was provided\n\nUsage: mv [OPTION]... [-T] SOURCE DEST\n mv [OPTION]... SOURCE... DIRECTORY\n mv [OPTION]... -t DIRECTORY SOURCE...\n/g" tests/mv/diag.sh # our error message is better @@ -311,7 +310,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 00000000000..d3d5595a2c5 --- /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 00000000000..3f57d204813 --- /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 00000000000..44e33723bc1 --- /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 fc8b988fea5..731abcc9117 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 00000000000..5944e3f5661 --- /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 diff --git a/util/update-version.sh b/util/update-version.sh index 47e1695ae29..237beb1008d 100755 --- a/util/update-version.sh +++ b/util/update-version.sh @@ -17,8 +17,8 @@ # 10) Create the release on github https://github.com/uutils/coreutils/releases/new # 11) Make sure we have good release notes -FROM="0.0.27" -TO="0.0.28" +FROM="0.0.28" +TO="0.0.29" PROGS=$(ls -1d src/uu/*/Cargo.toml src/uu/stdbuf/src/libstdbuf/Cargo.toml src/uucore/Cargo.toml Cargo.toml) diff --git a/util/why-error.md b/util/why-error.md new file mode 100644 index 00000000000..44c4a9e9728 --- /dev/null +++ b/util/why-error.md @@ -0,0 +1,73 @@ +This file documents why some tests are failing: + +* gnu/tests/chgrp/from.sh - https://github.com/uutils/coreutils/issues/7039 +* gnu/tests/cp/preserve-gid.sh +* gnu/tests/csplit/csplit-suppress-matched.pl +* gnu/tests/date/date-debug.sh +* gnu/tests/date/date-next-dow.pl +* gnu/tests/date/date-tz.sh +* gnu/tests/date/date.pl +* gnu/tests/dd/direct.sh +* gnu/tests/dd/no-allocate.sh +* gnu/tests/dd/nocache_eof.sh +* gnu/tests/dd/skip-seek-past-file.sh +* gnu/tests/dd/stderr.sh +* gnu/tests/du/long-from-unreadable.sh +* gnu/tests/du/move-dir-while-traversing.sh +* gnu/tests/expr/expr-multibyte.pl +* gnu/tests/expr/expr.pl +* gnu/tests/fmt/goal-option.sh +* gnu/tests/fmt/non-space.sh +* gnu/tests/head/head-elide-tail.pl +* gnu/tests/head/head-pos.sh +* gnu/tests/head/head-write-error.sh +* gnu/tests/help/help-version-getopt.sh +* gnu/tests/help/help-version.sh +* gnu/tests/ls/ls-misc.pl +* gnu/tests/ls/stat-free-symlinks.sh +* gnu/tests/misc/close-stdout.sh +* gnu/tests/misc/comm.pl +* gnu/tests/misc/kill.sh - https://github.com/uutils/coreutils/issues/7066 https://github.com/uutils/coreutils/issues/7067 +* gnu/tests/misc/nohup.sh +* gnu/tests/misc/numfmt.pl +* gnu/tests/misc/stdbuf.sh - https://github.com/uutils/coreutils/issues/7072 +* gnu/tests/misc/tee.sh - https://github.com/uutils/coreutils/issues/7073 +* gnu/tests/misc/time-style.sh +* gnu/tests/misc/tsort.pl - https://github.com/uutils/coreutils/issues/7074 +* gnu/tests/misc/write-errors.sh +* gnu/tests/mv/hard-link-1.sh +* gnu/tests/mv/mv-special-1.sh - https://github.com/uutils/coreutils/issues/7076 +* gnu/tests/mv/part-fail.sh +* gnu/tests/mv/part-hardlink.sh +* gnu/tests/od/od-N.sh +* gnu/tests/od/od-float.sh +* gnu/tests/printf/printf-cov.pl +* gnu/tests/printf/printf-indexed.sh +* gnu/tests/printf/printf-mb.sh +* gnu/tests/printf/printf-quote.sh +* gnu/tests/printf/printf.sh +* gnu/tests/ptx/ptx-overrun.sh +* gnu/tests/ptx/ptx.pl +* gnu/tests/rm/empty-inacc.sh - https://github.com/uutils/coreutils/issues/7033 +* gnu/tests/rm/ir-1.sh +* gnu/tests/rm/one-file-system.sh - https://github.com/uutils/coreutils/issues/7011 +* gnu/tests/rm/rm1.sh +* gnu/tests/rm/rm2.sh +* gnu/tests/shred/shred-passes.sh +* gnu/tests/sort/sort-continue.sh +* gnu/tests/sort/sort-debug-keys.sh +* gnu/tests/sort/sort-debug-warn.sh +* gnu/tests/sort/sort-files0-from.pl +* gnu/tests/sort/sort-float.sh +* gnu/tests/sort/sort-h-thousands-sep.sh +* gnu/tests/sort/sort-merge-fdlimit.sh +* gnu/tests/sort/sort-month.sh +* gnu/tests/sort/sort.pl +* gnu/tests/stat/stat-nanoseconds.sh +* gnu/tests/tac/tac-2-nonseekable.sh +* gnu/tests/tail/end-of-device.sh +* gnu/tests/tail/follow-stdin.sh +* gnu/tests/tail/inotify-rotate-resources.sh +* gnu/tests/tail/symlink.sh +* gnu/tests/touch/obsolescent.sh +* gnu/tests/tty/tty-eof.pl diff --git a/util/why-skip.txt b/util/why-skip.md similarity index 97% rename from util/why-skip.txt rename to util/why-skip.md index c790311c13d..40bb2a0093e 100644 --- a/util/why-skip.txt +++ b/util/why-skip.md @@ -63,10 +63,6 @@ tests/misc/csplit-heap.sh = multicall binary is disabled = tests/misc/coreutils.sh -= your ls doesn't call capget = -tests/ls/no-cap.sh - - = not running on GNU/Hurd = tests/id/gnu-zero-uids.sh