diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index 45f80915464..4385ae152dc 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -3,9 +3,9 @@ name: CICD # spell-checker:ignore (abbrev/names) CACHEDIR CICD CodeCOV MacOS MinGW MSVC musl taiki # spell-checker:ignore (env/flags) Awarnings Ccodegen Coverflow Cpanic Dwarnings RUSTDOCFLAGS RUSTFLAGS Zpanic CARGOFLAGS CLEVEL nodocs # spell-checker:ignore (jargon) SHAs deps dequote softprops subshell toolchain fuzzers dedupe devel profdata -# spell-checker:ignore (people) Peltoche rivy dtolnay Anson dawidd +# spell-checker:ignore (people) Peltoche rivy Anson dawidd # spell-checker:ignore (shell/tools) binutils choco clippy dmake esac fakeroot fdesc fdescfs gmake grcov halium lcov libclang libfuse libssl limactl nextest nocross pacman popd printf pushd redoxer rsync rustc rustfmt rustup shopt sccache utmpdump xargs zstd -# spell-checker:ignore (misc) aarch alnum armhf bindir busytest coreutils defconfig DESTDIR gecos getenforce gnueabihf issuecomment maint manpages msys multisize noconfirm nofeatures nullglob onexitbegin onexitend pell runtest Swatinem tempfile testsuite toybox uutils libsystemd codspeed wasip +# spell-checker:ignore (misc) aarch alnum armhf bindir busytest coreutils defconfig DESTDIR gecos getenforce gnueabihf issuecomment maint manpages msys multisize noconfirm nofeatures nullglob onexitbegin onexitend pell runtest tempfile testsuite toybox uutils libsystemd codspeed wasip libexecinfo env: PROJECT_NAME: coreutils @@ -102,7 +102,7 @@ jobs: # for now, don't build it on mac & windows because the doc is only published from linux # + it needs a bunch of duplication for build # and I don't want to add a doc step in the regular build to avoid long builds -# - { os: macos-latest , features: feat_os_macos } +# - { os: macos-latest , features: feat_os_unix } # - { os: windows-latest , features: feat_os_windows } steps: - uses: actions/checkout@v6 @@ -152,7 +152,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@v22 + - uses: DavidAnson/markdownlint-cli2-action@v23 with: fix: "true" globs: | @@ -237,7 +237,7 @@ jobs: RUST_BACKTRACE: "1" - name: Upload test results to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5 + uses: codecov/codecov-action@v6 with: token: ${{ secrets.CODECOV_TOKEN }} report_type: test_results @@ -277,7 +277,7 @@ jobs: matrix: job: - { os: ubuntu-latest , features: feat_os_unix } - - { os: macos-latest , features: feat_os_macos } + - { os: macos-latest , features: feat_os_unix } - { os: windows-latest , features: feat_os_windows } steps: - uses: actions/checkout@v6 @@ -301,7 +301,7 @@ jobs: RUST_BACKTRACE: "1" - name: Upload test results to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5 + uses: codecov/codecov-action@v6 with: token: ${{ secrets.CODECOV_TOKEN }} report_type: test_results @@ -320,7 +320,7 @@ jobs: matrix: job: - { os: ubuntu-latest , features: feat_os_unix } - - { os: macos-latest , features: feat_os_macos } + - { os: macos-latest , features: feat_os_unix } - { os: windows-latest , features: feat_os_windows } steps: - uses: actions/checkout@v6 @@ -344,7 +344,7 @@ jobs: RUST_BACKTRACE: "1" - name: Upload test results to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5 + uses: codecov/codecov-action@v6 with: token: ${{ secrets.CODECOV_TOKEN }} report_type: test_results @@ -378,13 +378,13 @@ jobs: - { os: ubuntu-latest , target: x86_64-unknown-linux-gnu , features: "feat_os_unix,test_risky_names", use-cross: use-cross, skip-publish: true } - { os: ubuntu-latest , target: x86_64-unknown-linux-gnu , features: "feat_os_unix,uudoc" , use-cross: no, workspace-tests: true } - { os: ubuntu-latest , target: x86_64-unknown-linux-musl , features: feat_os_unix_musl , use-cross: use-cross } - - { os: ubuntu-latest , target: x86_64-unknown-netbsd, features: "feat_Tier1,feat_require_unix_core,feat_require_unix_hostid,feat_require_unix_utmpx", use-cross: use-cross , skip-tests: true , check-only: true } + - { os: ubuntu-latest , target: x86_64-unknown-netbsd, features: "feat_os_unix", use-cross: use-cross , skip-tests: true , check-only: true } - { os: ubuntu-latest , target: x86_64-unknown-redox , features: feat_os_unix_redox , use-cross: redoxer , skip-tests: true , check-only: true } - { os: ubuntu-latest , target: wasm32-wasip1, default-features: false, features: feat_wasm, skip-tests: true } - - { os: macos-latest , target: aarch64-apple-darwin , features: feat_os_macos, workspace-tests: true } # M1 CPU + - { os: macos-latest , target: aarch64-apple-darwin , features: feat_os_unix, workspace-tests: true } # M1 CPU # PR #7964: chcon should not break build without the feature. cargo check is enough to detect it. - { os: macos-latest , target: aarch64-apple-darwin , workspace-tests: true, check-only: true } # M1 CPU - - { os: macos-latest , target: x86_64-apple-darwin , features: feat_os_macos, workspace-tests: true } + - { os: macos-latest , target: x86_64-apple-darwin , features: feat_os_unix, workspace-tests: true } - { os: windows-latest , target: i686-pc-windows-msvc , features: feat_os_windows } - { os: windows-latest , target: x86_64-pc-windows-gnu , features: feat_os_windows } - { os: windows-latest , target: x86_64-pc-windows-msvc , features: feat_os_windows } @@ -504,9 +504,16 @@ jobs: ;; esac - uses: taiki-e/install-action@v2 - if: steps.vars.outputs.CARGO_CMD == 'cross' + # `cross` v0.2.5 lacks `libexecinfo` on NetBSD. However, this has been added in main. + # See https://github.com/cross-rs/cross/blob/main/docker/netbsd.sh + # We are pulling cross from a specific commit hash rather than HEAD to avoid any potential breakage that may be introduced. + # Once `cross` v0.3 is out, these conditionals can be removed. + if: steps.vars.outputs.CARGO_CMD == 'cross' && matrix.job.target != 'x86_64-unknown-netbsd' with: tool: cross@0.2.5 + - name: Install cross from git (NetBSD) + if: steps.vars.outputs.CARGO_CMD == 'cross' && matrix.job.target == 'x86_64-unknown-netbsd' + run: cargo install cross --git https://github.com/cross-rs/cross --rev f86fd03bb70b4c6802847c18087e21391498b0b4 - name: Create all needed build/work directories shell: bash run: | @@ -603,7 +610,7 @@ jobs: if: matrix.job.skip-publish != true && matrix.job.check-only == true run: | # expr breaks redox - sed -i.b '/"expr",/d' Cargo.toml + if [[ "${{ matrix.job.target }}" == *"redox"* ]]; then sed -i.b '/"expr",/d' Cargo.toml; fi ${{ steps.vars.outputs.CARGO_CMD }} ${{ steps.vars.outputs.CARGO_CMD_OPTIONS }} check \ --target=${{ matrix.job.target }} ${{ matrix.job.cargo-options }} ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} ${{ steps.vars.outputs.CARGO_DEFAULT_FEATURES_OPTION }} - name: Test @@ -624,7 +631,7 @@ jobs: ${{ steps.vars.outputs.CARGO_CMD }} ${{ steps.vars.outputs.CARGO_CMD_OPTIONS }} build --release --config=profile.release.strip=true \ --target=${{ matrix.job.target }} ${{ matrix.job.cargo-options }} ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} ${{ steps.vars.outputs.CARGO_DEFAULT_FEATURES_OPTION }} # We don't want to have many duplicated long jobs at here - # So we build individual binaries for few platforms until we deduplicate many release build for Linux + # So we build individual binaries for few platforms until we deduplicate many release build for Linux - name: Build individual binaries if: matrix.job.skip-tests != true && matrix.job.target == 'x86_64-pc-windows-msvc' shell: bash @@ -798,7 +805,7 @@ jobs: # RUSTUP_TOOLCHAIN: ${{ steps.vars.outputs.TOOLCHAIN }} - name: Upload coverage results (to Codecov.io) - uses: codecov/codecov-action@v5 + uses: codecov/codecov-action@v6 with: token: ${{ secrets.CODECOV_TOKEN }} files: ${{ steps.run_test_cov.outputs.report }} @@ -808,7 +815,7 @@ jobs: fail_ci_if_error: false - name: Upload test results to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5 + uses: codecov/codecov-action@v6 with: token: ${{ secrets.CODECOV_TOKEN }} report_type: test_results @@ -859,7 +866,7 @@ jobs: fail-fast: false matrix: job: - - { os: macos-latest , features: feat_os_macos } + - { os: macos-latest , features: feat_os_unix } - { os: windows-latest , features: feat_os_windows } steps: diff --git a/.github/workflows/FixPR.yml b/.github/workflows/FixPR.yml index d086687e86d..224617c6d3e 100644 --- a/.github/workflows/FixPR.yml +++ b/.github/workflows/FixPR.yml @@ -1,6 +1,6 @@ name: FixPR -# spell-checker:ignore Swatinem dtolnay dedupe +# spell-checker:ignore dedupe # Trigger automated fixes for PRs being merged (with associated commits) @@ -69,7 +69,7 @@ jobs: ## * using the 'stable' toolchain is necessary to avoid "unexpected '--filter-platform'" errors cargo +stable tree --locked --no-dedupe -e=no-dev --prefix=none --features ${{ matrix.job.features }} | grep -vE "$PWD" | sort --unique - name: Commit any changes (to '${{ env.BRANCH_TARGET }}') - uses: EndBug/add-and-commit@v9 + uses: EndBug/add-and-commit@v10 with: new_branch: ${{ env.BRANCH_TARGET }} default_author: github_actions diff --git a/.github/workflows/GnuTests.yml b/.github/workflows/GnuTests.yml index 2ad489c8f18..f91941feb8d 100644 --- a/.github/workflows/GnuTests.yml +++ b/.github/workflows/GnuTests.yml @@ -1,10 +1,10 @@ name: GnuTests -# spell-checker:ignore (abbrev/names) CodeCov gnulib GnuTests Swatinem +# spell-checker:ignore (abbrev/names) CodeCov gnulib GnuTests # spell-checker:ignore (jargon) submodules devel -# spell-checker:ignore (libs/utils) chksum dpkg getenforce getlimits gperf lcov libexpect limactl pyinotify setenforce shopt valgrind libattr libcap taiki-e zstd cpio +# spell-checker:ignore (libs/utils) chksum dpkg getenforce gperf lcov libexpect limactl pyinotify setenforce shopt valgrind libattr libcap taiki-e zstd cpio # spell-checker:ignore (options) Ccodegen Coverflow Cpanic Zpanic -# spell-checker:ignore (people) Dawid Dziurla * dawidd dtolnay +# spell-checker:ignore (people) Dawid Dziurla * dawidd # spell-checker:ignore (vars) FILESET SUBDIRS XPASS # spell-checker:ignore userns nodocs @@ -354,7 +354,7 @@ jobs: path: 'uutils' persist-credentials: false - name: Retrieve reference artifacts - uses: dawidd6/action-download-artifact@v19 + uses: dawidd6/action-download-artifact@v20 # ref: continue-on-error: true ## don't break the build for missing reference artifacts (may be expired or just not generated yet) with: diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index eeb68292d00..2837c79f60a 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -1,6 +1,6 @@ name: Android -# spell-checker:ignore (people) reactivecircus Swatinem dtolnay juliangruber +# spell-checker:ignore (people) reactivecircus juliangruber # spell-checker:ignore (shell/tools) TERMUX nextest udevadm pkill # spell-checker:ignore (misc) swiftshader playstore DATALOSS noaudio diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 8466b564962..9c8c180bf45 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -1,6 +1,6 @@ name: Benchmarks -# spell-checker:ignore (people) dtolnay Swatinem taiki-e +# spell-checker:ignore (people) taiki-e # spell-checker:ignore (misc) codspeed sccache on: @@ -30,6 +30,7 @@ jobs: uu_base64, uu_cksum, uu_cp, + uu_cat, uu_cut, uu_dd, uu_df, @@ -47,6 +48,7 @@ jobs: uu_shuf, uu_sort, uu_split, + uu_timeout, uu_tsort, uu_unexpand, uu_uniq, diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index adbd1ec268b..7b7bf87e06a 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -1,6 +1,6 @@ name: Code Quality -# spell-checker:ignore (people) dtolnay juliangruber pell reactivecircus Swatinem taiki-e taplo +# spell-checker:ignore (people) juliangruber pell reactivecircus taiki-e taplo # spell-checker:ignore (misc) TERMUX noaudio pkill swiftshader esac sccache pcoreutils shopt subshell dequote libsystemd on: @@ -72,8 +72,9 @@ jobs: matrix: job: - { os: ubuntu-latest , features: all , workspace: true } - - { os: macos-latest , features: feat_os_macos } + - { os: macos-latest , features: feat_os_unix } - { os: windows-latest , features: feat_os_windows } + - { os: ubuntu-latest , features: feat_wasm , target: wasm32-wasip1 } steps: - uses: actions/checkout@v6 with: @@ -82,6 +83,7 @@ jobs: with: toolchain: stable components: clippy + targets: ${{ matrix.job.target || '' }} - uses: Swatinem/rust-cache@v2 - name: Run sccache-cache id: sccache-setup @@ -105,6 +107,7 @@ jobs: esac; outputs FAIL_ON_FAULT FAULT_TYPE - name: Install/setup prerequisites + if: ${{ ! matrix.job.target }} shell: bash run: | ## Install/setup prerequisites @@ -116,7 +119,7 @@ jobs: ;; esac - name: "`cargo clippy` lint testing" - uses: nick-fields/retry@v3 + uses: nick-fields/retry@v4 with: max_attempts: 3 retry_on: error @@ -124,31 +127,20 @@ jobs: shell: bash command: | ## `cargo clippy` lint testing - unset fault - fault_type="${{ steps.vars.outputs.FAULT_TYPE }}" - fault_prefix=$(echo "$fault_type" | tr '[:lower:]' '[:upper:]') - # * convert any warnings to GHA UI annotations; ref: - if [[ "${{ matrix.job.features }}" == "all" ]]; then - extra="--all-features" - else - extra="--features ${{ matrix.job.features }}" + ARGS="--features ${{ matrix.job.features }}" + ARGS="${ARGS} --fault-type ${{ steps.vars.outputs.FAULT_TYPE }}" + if [[ "${{ matrix.job.workspace }}" =~ ^(1|t|true|y|yes)$ ]]; then + ARGS="${ARGS} --workspace" fi - case '${{ matrix.job.workspace }}' in - 1|t|true|y|yes) - extra="${extra} --workspace" - ;; - esac - # * determine sub-crate utility list (similar to FreeBSD workflow) - if [[ "${{ matrix.job.features }}" == "all" ]]; then - UTILITY_LIST="$(./util/show-utils.sh --all-features)" - else - UTILITY_LIST="$(./util/show-utils.sh --features ${{ matrix.job.features }})" + if [[ -n "${{ matrix.job.target }}" ]]; then + ARGS="${ARGS} --target ${{ matrix.job.target }}" fi - CARGO_UTILITY_LIST_OPTIONS="$(for u in ${UTILITY_LIST}; do echo -n "-puu_${u} "; done;)" - S=$(cargo clippy --all-targets $extra --tests --benches -pcoreutils ${CARGO_UTILITY_LIST_OPTIONS} -- -D warnings 2>&1) && printf "%s\n" "$S" || { printf "%s\n" "$S" ; printf "%s" "$S" | sed -E -n -e '/^error:/{' -e "N; s/^error:[[:space:]]+(.*)\\n[[:space:]]+-->[[:space:]]+(.*):([0-9]+):([0-9]+).*$/::${fault_type} file=\2,line=\3,col=\4::${fault_prefix}: \`cargo clippy\`: \1 (file:'\2', line:\3)/p;" -e '}' ; fault=true ; } - if [ -n "${{ steps.vars.outputs.FAIL_ON_FAULT }}" ] && [ -n "$fault" ]; then exit 1 ; fi + if [[ -n "${{ steps.vars.outputs.FAIL_ON_FAULT }}" ]]; then + ARGS="${ARGS} --fail-on-fault" + fi + python3 util/run-clippy.py ${ARGS} - name: "cargo clippy on fuzz dir" - if: runner.os != 'Windows' + if: runner.os != 'Windows' && !matrix.job.target shell: bash run: | cd fuzz diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 53f830a7911..ae5e8d3c8c1 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -1,4 +1,4 @@ -# spell-checker:ignore dtolnay libsystemd libattr libcap gsub +# spell-checker:ignore libsystemd libattr libcap gsub name: Check uudoc Documentation Generation diff --git a/.github/workflows/freebsd.yml b/.github/workflows/freebsd.yml index f963cf3ef13..91879538877 100644 --- a/.github/workflows/freebsd.yml +++ b/.github/workflows/freebsd.yml @@ -1,6 +1,6 @@ name: FreeBSD -# spell-checker:ignore sshfs usesh vmactions taiki Swatinem esac fdescfs fdesc nextest copyback logind +# spell-checker:ignore sshfs usesh vmactions taiki esac fdescfs fdesc nextest copyback logind env: # * style job configuration @@ -41,7 +41,7 @@ jobs: sync: rsync copyback: false # We need jq and GNU coreutils to run show-utils.sh and bash to use inline shell string replacement - prepare: pkg install -y curl sudo jq coreutils bash + prepare: pkg install -y curl sudo jq coreutils bash python3 run: | ## Prepare, build, and test # implementation modelled after ref: @@ -73,7 +73,6 @@ jobs: FAULT_PREFIX=\$(echo "\${FAULT_TYPE}" | tr '[:lower:]' '[:upper:]') # * determine sub-crate utility list UTILITY_LIST="\$(./util/show-utils.sh --features ${{ matrix.job.features }})" - CARGO_UTILITY_LIST_OPTIONS="\$(for u in \${UTILITY_LIST}; do echo -n "-puu_\${u} "; done;)" ## Info # environment echo "## environment" @@ -101,8 +100,9 @@ jobs: ## cargo clippy lint testing if [ -z "\${FAULT}" ]; then echo "## cargo clippy lint testing" - # * convert any warnings to GHA UI annotations; ref: - S=\$(cargo clippy --all-targets \${CARGO_UTILITY_LIST_OPTIONS} -- -D warnings 2>&1) && printf "%s\n" "\$S" || { printf "%s\n" "\$S" ; printf "%s" "\$S" | sed -E -n -e '/^error:/{' -e "N; s/^error:[[:space:]]+(.*)\\n[[:space:]]+-->[[:space:]]+(.*):([0-9]+):([0-9]+).*\$/::\${FAULT_TYPE} file=\2,line=\3,col=\4::\${FAULT_PREFIX}: \\\`cargo clippy\\\`: \1 (file:'\2', line:\3)/p;" -e '}' ; FAULT=true ; } + CLIPPY_ARGS="--features ${{ matrix.job.features }} --fault-type \${FAULT_TYPE}" + if [ -n "\${FAIL_ON_FAULT}" ]; then CLIPPY_ARGS="\${CLIPPY_ARGS} --fail-on-fault"; fi + python3 util/run-clippy.py \${CLIPPY_ARGS} || FAULT=true fi # Clean to avoid to rsync back the files cargo clean diff --git a/.github/workflows/fuzzing.yml b/.github/workflows/fuzzing.yml index 034f5384f38..47ee6477191 100644 --- a/.github/workflows/fuzzing.yml +++ b/.github/workflows/fuzzing.yml @@ -1,6 +1,6 @@ name: Fuzzing -# spell-checker:ignore (people) dtolnay Swatinem taiki-e +# spell-checker:ignore (people) taiki-e # spell-checker:ignore (misc) fuzzer env: diff --git a/.github/workflows/ignore-intermittent.txt b/.github/workflows/ignore-intermittent.txt index 019b3d86ed6..c09fc2dd5f0 100644 --- a/.github/workflows/ignore-intermittent.txt +++ b/.github/workflows/ignore-intermittent.txt @@ -1,6 +1,7 @@ tests/cut/bounded-memory tests/date/date-locale-hour tests/date/resolution +tests/expand/bounded-memory tests/pr/bounded-memory tests/tail/inotify-dir-recreate tests/tail/overlay-headers @@ -13,3 +14,4 @@ tests/misc/stdbuf tests/misc/usage_vs_getopt tests/misc/tee tests/tail/follow-name +tests/rm/isatty diff --git a/.github/workflows/l10n.yml b/.github/workflows/l10n.yml index 57c4d51ef21..11b2c52e748 100644 --- a/.github/workflows/l10n.yml +++ b/.github/workflows/l10n.yml @@ -31,7 +31,7 @@ jobs: matrix: job: - { os: ubuntu-latest , features: "feat_os_unix" } - - { os: macos-latest , features: "feat_os_macos" } + - { os: macos-latest , features: "feat_os_unix" } - { os: windows-latest , features: "feat_os_windows" } steps: - uses: actions/checkout@v6 @@ -56,17 +56,24 @@ jobs: ubuntu-*) # selinux headers needed for testing sudo apt-get -y update ; sudo apt-get -y install libselinux1-dev + # Locales required by tests/by-util/test_date.rs and + # tests/by-util/test_dd.rs (fr_FR is the ISO-8859-1 variant + # used by test_iso8859_1_case_conversion). + sudo locale-gen --keep-existing fr_FR + sudo locale-gen --keep-existing fr_FR.UTF-8 + sudo locale-gen --keep-existing es_ES.UTF-8 + sudo locale-gen --keep-existing en_US.UTF-8 + sudo locale-gen --keep-existing fa_IR.UTF-8 # Iran + sudo locale-gen --keep-existing am_ET.UTF-8 # Ethiopia + sudo locale-gen --keep-existing th_TH.UTF-8 # Thailand + sudo locale-gen --keep-existing hu_HU.UTF-8 # Hungary + sudo update-locale ;; macos-*) # needed for testing brew install coreutils ;; esac - - name: Build with platform features - shell: bash - run: | - ## Build with platform-specific features to enable l10n functionality - cargo build --features ${{ matrix.job.features }} - name: Test l10n functionality shell: bash run: | @@ -149,10 +156,6 @@ jobs: sudo apt-get -y update ; sudo apt-get -y install libselinux1-dev sudo locale-gen --keep-existing fr_FR.UTF-8 locale -a | grep -i fr || exit 1 - - name: Build coreutils with clap localization support - shell: bash - run: | - cargo build --features feat_os_unix --bin coreutils - name: Test English clap error localization shell: bash run: | @@ -321,11 +324,6 @@ jobs: ## Generate French locale for testing sudo locale-gen --keep-existing fr_FR.UTF-8 locale -a | grep -i fr || echo "French locale not found, continuing anyway" - - name: Build coreutils with l10n support - shell: bash - run: | - ## Build coreutils with Unix features and l10n support - cargo build --features feat_os_unix --bin coreutils - name: Test French localization shell: bash run: | @@ -411,7 +409,7 @@ jobs: matrix: job: - { os: ubuntu-latest , features: "feat_os_unix" } - - { os: macos-latest , features: "feat_os_macos" } + - { os: macos-latest , features: "feat_os_unix" } steps: - uses: actions/checkout@v6 with: @@ -564,7 +562,7 @@ jobs: matrix: job: - { os: ubuntu-latest , features: "feat_os_unix" } - - { os: macos-latest , features: "feat_os_macos" } + - { os: macos-latest , features: "feat_os_unix" } steps: - uses: actions/checkout@v6 with: @@ -706,7 +704,7 @@ jobs: mkdir -p "$CARGO_INSTALL_DIR" # Install using cargo with l10n features - cargo install --path . --features ${{ matrix.job.features }} --root "$CARGO_INSTALL_DIR" --locked + cargo install --path . --features "ls,cat,touch" --root "$CARGO_INSTALL_DIR" --locked # Verify installation echo "Testing cargo-installed binaries..." @@ -1379,11 +1377,3 @@ jobs: echo "::warning::More locales than expected ($total_match_count entries)" echo "This might be expected for utility + uucore locales" fi - - l10n_locale_embedding_regression_test: - name: L10n/Locale Embedding Regression Test - runs-on: ubuntu-latest - needs: [l10n_locale_embedding_cat, l10n_locale_embedding_ls, l10n_locale_embedding_multicall, l10n_locale_embedding_cargo_install] - steps: - - name: All locale embedding tests passed - run: echo "✓ All locale embedding tests passed successfully" diff --git a/.github/workflows/make.yml b/.github/workflows/make.yml index 7fc96461216..936b60669ca 100644 --- a/.github/workflows/make.yml +++ b/.github/workflows/make.yml @@ -5,7 +5,7 @@ name: make # spell-checker:ignore (jargon) deps softprops toolchain # spell-checker:ignore (people) dawidd # spell-checker:ignore (shell/tools) nextest sccache zstd -# spell-checker:ignore (misc) bindir busytest defconfig DESTDIR manpages multisize runtest Swatinem testsuite toybox uutils +# spell-checker:ignore (misc) bindir busytest defconfig DESTDIR manpages multisize runtest testsuite toybox uutils env: PROJECT_NAME: coreutils @@ -85,7 +85,7 @@ jobs: RUST_BACKTRACE: "1" - name: Upload test results to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5 + uses: codecov/codecov-action@v6 with: token: ${{ secrets.CODECOV_TOKEN }} report_type: test_results @@ -201,14 +201,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@v19 + uses: dawidd6/action-download-artifact@v20 with: workflow: make.yml name: individual-size-result repo: uutils/coreutils path: dl - name: Download the previous size result - uses: dawidd6/action-download-artifact@v19 + uses: dawidd6/action-download-artifact@v20 with: workflow: make.yml name: size-result @@ -266,6 +266,40 @@ jobs: # 2. the makefile doesn't try to install libstdbuf even though stdbuf is skipped DESTDIR=/tmp/ make SKIP_UTILS="stdbuf" install + # keep this job minimal to avoid have many duplicated build with CICD + build_makefile-other: + name: Build/Makefile + runs-on: ${{ matrix.job.os }} + env: + CARGO_INCREMENTAL: 0 + strategy: + fail-fast: false + matrix: + job: + - { os: windows-latest , features: feat_os_windows } + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + - uses: Swatinem/rust-cache@v2 + - name: Run sccache-cache + id: sccache-setup + uses: mozilla-actions/sccache-action@v0.0.9 + continue-on-error: true + - name: Export sccache + if: steps.sccache-setup.outcome == 'success' + run: | + echo "RUSTC_WRAPPER=sccache" >> $GITHUB_ENV + echo "SCCACHE_GHA_ENABLED=true" >> $GITHUB_ENV + - name: "`make build`" + shell: bash + run: | + set -x + # Check that we exclude unix programs to avoid build failure + make PREFIX=/tmp/usr MULTICALL=y COMPLETIONS=n MANPAGES=n LOCALES=n \ + SKIP_UTILS="arch b2sum base32 base64 basename basenc cat cksum comm cp csplit cut date dd df dir dircolors dirname du echo env expand expr factor false fmt fold head hostname join link ln ls md5sum mkdir mktemp more mv nl nproc numfmt od paste pr printenv printf ptx pwd readlink realpath rm rmdir seq sha1sum sha224sum sha256sum sha384sum sha512sum shred shuf sleep sort split sum sync tac tail tee test touch tr truncate tsort uname unexpand uniq unlink vdir wc whoami yes" + target/debug/coreutils.exe true + test_busybox: name: Tests/BusyBox test suite runs-on: ${{ matrix.job.os }} diff --git a/.github/workflows/manpage-lint.yml b/.github/workflows/manpage-lint.yml index fe30d79afa7..c294e01a26b 100644 --- a/.github/workflows/manpage-lint.yml +++ b/.github/workflows/manpage-lint.yml @@ -1,4 +1,4 @@ -# spell-checker:ignore mandoc uudoc manpages dtolnay libsystemd libattr libcap DESTDIR +# spell-checker:ignore mandoc uudoc manpages libsystemd libattr libcap DESTDIR name: Manpage Validation diff --git a/.github/workflows/openbsd.yml b/.github/workflows/openbsd.yml index dea831b108c..797185e04c9 100644 --- a/.github/workflows/openbsd.yml +++ b/.github/workflows/openbsd.yml @@ -1,6 +1,6 @@ name: OpenBSD -# spell-checker:ignore sshfs usesh vmactions taiki Swatinem esac fdescfs fdesc sccache nextest copyback logind bindgen libclang +# spell-checker:ignore sshfs usesh vmactions taiki esac fdescfs fdesc sccache nextest copyback logind bindgen libclang env: # * style job configuration @@ -47,7 +47,7 @@ jobs: prepare: | # Clean up disk space before installing packages df -h - pkg_add curl sudo-- jq coreutils bash rust rust-clippy rust-rustfmt llvm-- + pkg_add curl sudo-- jq coreutils bash rust rust-clippy rust-rustfmt llvm-- python3 rm -rf /usr/share/relink/* /usr/X11R6/* /usr/share/doc/* /usr/share/man/* & # Clean up package cache after installation pkg_delete -a & @@ -84,7 +84,6 @@ jobs: FAULT_PREFIX=\$(echo "\${FAULT_TYPE}" | tr '[:lower:]' '[:upper:]') # * determine sub-crate utility list UTILITY_LIST="\$(./util/show-utils.sh --features ${{ matrix.job.features }})" - CARGO_UTILITY_LIST_OPTIONS="\$(for u in \${UTILITY_LIST}; do echo -n "-puu_\${u} "; done;)" ## Info # environment echo "## environment" @@ -111,8 +110,9 @@ jobs: ## cargo clippy lint testing if [ -z "\${FAULT}" ]; then echo "## cargo clippy lint testing" - # * convert any warnings to GHA UI annotations; ref: - S=\$(cargo clippy --all-targets \${CARGO_UTILITY_LIST_OPTIONS} -- -D warnings 2>&1) && printf "%s\n" "\$S" || { printf "%s\n" "\$S" ; printf "%s" "\$S" | sed -E -n -e '/^error:/{' -e "N; s/^error:[[:space:]]+(.*)\\n[[:space:]]+-->[[:space:]]+(.*):([0-9]+):([0-9]+).*\$/::\${FAULT_TYPE} file=\2,line=\3,col=\4::\${FAULT_PREFIX}: \\\`cargo clippy\\\`: \1 (file:'\2', line:\3)/p;" -e '}' ; FAULT=true ; } + CLIPPY_ARGS="--features ${{ matrix.job.features }} --fault-type \${FAULT_TYPE}" + if [ -n "\${FAIL_ON_FAULT}" ]; then CLIPPY_ARGS="\${CLIPPY_ARGS} --fail-on-fault"; fi + python3 util/run-clippy.py \${CLIPPY_ARGS} || FAULT=true fi # Clean to avoid to rsync back the files and free up disk space cargo clean diff --git a/.github/workflows/wasi.yml b/.github/workflows/wasi.yml new file mode 100644 index 00000000000..ba5e5ac3472 --- /dev/null +++ b/.github/workflows/wasi.yml @@ -0,0 +1,42 @@ +# spell-checker:ignore wasip wasmtime +name: WASI + +# spell-checker:ignore TRIGGERPATH +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +# End the current execution if there is a new changeset in the PR. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + test_wasi: + name: Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + - uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-wasip1 + - uses: Swatinem/rust-cache@v2 + - name: Install wasmtime + run: | + curl https://wasmtime.dev/install.sh -sSf | bash + echo "$HOME/.wasmtime/bin" >> $GITHUB_PATH + - name: Run tests + env: + CARGO_TARGET_WASM32_WASIP1_RUNNER: wasmtime + run: | + # Get all utilities and exclude ones that don't compile for wasm32-wasip1 + EXCLUDE="dd|df|du|env|expr|mktemp|more|tac|test" + UTILS=$(./util/show-utils.sh | tr ' ' '\n' | grep -vE "^($EXCLUDE)$" | sed 's/^/-p uu_/' | tr '\n' ' ') + cargo test --target wasm32-wasip1 --no-default-features $UTILS diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 845b03f1a80..2ca1cdab94c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,6 @@ # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks +exclude: ^tests/fixtures/ repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 @@ -43,9 +44,19 @@ repos: pass_filenames: false types: [file, rust] language: system + - id: cargo-lock-check + name: Cargo.lock sync check + description: Ensure Cargo.lock and fuzz/Cargo.lock are up-to-date. + entry: bash -c 'for dir in . fuzz; do ( cd "$dir" && cargo fetch --quiet ); done' + pass_filenames: false + files: 'Cargo\.(toml|lock)$' + language: system - id: cspell name: Code spell checker (cspell) description: Run cspell to check for spelling errors (if available). entry: bash -c 'if command -v cspell >/dev/null 2>&1; then cspell --no-must-find-files -- "$@"; else echo "cspell not found, skipping spell check"; exit 0; fi' -- pass_filenames: true language: system + +ci: + skip: [rust-linting, rust-clippy, cargo-lock-check, cspell] diff --git a/.vscode/cspell.dictionaries/jargon.wordlist.txt b/.vscode/cspell.dictionaries/jargon.wordlist.txt index 2f26340acdc..16778da940d 100644 --- a/.vscode/cspell.dictionaries/jargon.wordlist.txt +++ b/.vscode/cspell.dictionaries/jargon.wordlist.txt @@ -215,6 +215,13 @@ vals inval nofield +# ci +dtolnay +Swatinem + +preopened +dotdot + # * clippy uninlined nonminimal diff --git a/.vscode/cspell.dictionaries/workspace.wordlist.txt b/.vscode/cspell.dictionaries/workspace.wordlist.txt index 1d3929832a2..3845e4d0411 100644 --- a/.vscode/cspell.dictionaries/workspace.wordlist.txt +++ b/.vscode/cspell.dictionaries/workspace.wordlist.txt @@ -8,6 +8,7 @@ advapi32-sys aho-corasick backtrace blake2b_simd +rustix # * uutils project uutils @@ -138,6 +139,7 @@ EEXIST EINVAL ENODATA ENOENT +ENOSPC ENOSYS ENOTEMPTY EOPNOTSUPP @@ -183,9 +185,13 @@ LINESIZE NAMESIZE RTLD_NEXT RTLD +RTMAX +RTMIN SIGABRT SIGINT SIGKILL +SIGRTMAX +SIGRTMIN SIGSTOP SIGTERM SYS_fdatasync @@ -360,9 +366,12 @@ uutests uutils # * function names +execfn getcwd +setpipe # * other +getlimits weblate algs wasm diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a8e463707f0..b614f44396f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -51,6 +51,8 @@ crates is as follows: We have separated repositories for crates that we maintain but also publish for use by others: +- [coreutils-i10n](https://github.com/uutils/coreutils-l10n) +- [num-prime](https://github.com/uutils/num-prime) - [uutils-term-grid](https://github.com/uutils/uutils-term-grid) - [parse_datetime](https://github.com/uutils/parse_datetime) @@ -78,6 +80,7 @@ issues and writing documentation are just as important as writing code. We can't fix bugs we don't know about, so good issues are super helpful! Here are some tips for writing good issues: +- Confirm the bug is in coreutils; some tools (e.g., `find`, `sed`) are maintained in separate repositories under the uutils project. - If you find a bug, make sure it's still a problem on the [`main` branch](https://github.com/uutils/coreutils/releases/tag/latest-commit). - Search through the existing issues to see whether it has already been reported. @@ -123,7 +126,7 @@ submit a patch! ### Don't `panic!` The coreutils should be very reliable. This means that we should never `panic!`. -Therefore, you should avoid using `.unwrap()` and `panic!`. Sometimes the use of +Therefore, you should avoid using `println!`, `.unwrap()` and `panic!`. Sometimes the use of `unreachable!` can be justified with a comment explaining why that code is unreachable. @@ -260,9 +263,6 @@ you contribute must at least compile without warnings for all platforms in the CI. However, you can use `#[cfg(...)]` attributes to create platform dependent features. -**Tip:** For Windows, Microsoft provides some images (VMWare, Hyper-V, -VirtualBox and Parallels) for development [on their official download page](https://developer.microsoft.com/windows/downloads/virtual-machines/). - ## Improving the GNU compatibility Please make sure you have installed diff --git a/Cargo.lock b/Cargo.lock index a34facd0566..eb641725a0d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -213,16 +213,16 @@ dependencies = [ [[package]] name = "blake3" -version = "1.8.3" +version = "1.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d" +checksum = "4d2d5991425dfd0785aed03aedcf0b321d61975c9b5b3689c774a2610ae0b51e" dependencies = [ "arrayref", "arrayvec", "cc", "cfg-if", "constant_time_eq", - "cpufeatures 0.2.17", + "cpufeatures 0.3.0", ] [[package]] @@ -234,6 +234,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +dependencies = [ + "hybrid-array", +] + [[package]] name = "block2" version = "0.6.2" @@ -274,9 +283,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "calendrical_calculations" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a0b39595c6ee54a8d0900204ba4c401d0ab4eb45adaf07178e8d017541529e7" +checksum = "5abbd6eeda6885048d357edc66748eea6e0268e3dd11f326fff5bd248d779c26" dependencies = [ "core_maths", "displaydoc", @@ -385,9 +394,9 @@ checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" [[package]] name = "clap_mangen" -version = "0.2.33" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e30ffc187e2e3aeafcd1c6e2aa416e29739454c0ccaa419226d5ecd181f2d78" +checksum = "d82842b45bf9f6a3be090dd860095ac30728042c08e0d6261ca7259b5d850f07" dependencies = [ "clap", "roff", @@ -493,6 +502,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "const-random" version = "0.1.18" @@ -536,7 +551,7 @@ dependencies = [ [[package]] name = "coreutils" -version = "0.7.0" +version = "0.8.0" dependencies = [ "bytecount", "clap", @@ -559,7 +574,9 @@ dependencies = [ "regex", "rlimit", "rstest", + "rstest_reuse", "rustc-hash", + "rustix", "selinux", "sha1", "tempfile", @@ -721,7 +738,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fd92aca2c6001b1bf5ba0ff84ee74ec8501b52bbef0cac80bf25a6c1d87a83d" dependencies = [ "crc", - "digest", + "digest 0.10.7", "rustversion", "spin", ] @@ -803,11 +820,20 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" +dependencies = [ + "hybrid-array", +] + [[package]] name = "ctor" -version = "0.6.3" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "424e0138278faeb2b401f174ad17e715c829512d74f3d1e81eb43365c2e0590e" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" dependencies = [ "ctor-proc-macro", "dtor", @@ -877,8 +903,19 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "crypto-common", + "block-buffer 0.10.4", + "crypto-common 0.1.7", +] + +[[package]] +name = "digest" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" +dependencies = [ + "block-buffer 0.12.0", + "const-oid", + "crypto-common 0.2.1", ] [[package]] @@ -947,9 +984,9 @@ dependencies = [ [[package]] name = "dtor" -version = "0.1.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "404d02eeb088a82cfd873006cb713fe411306c7d182c344905e101fb1167d301" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" dependencies = [ "dtor-proc-macro", ] @@ -1048,9 +1085,9 @@ checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "fixed_decimal" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35eabf480f94d69182677e37571d3be065822acfafd12f2f085db44fbbcc8e57" +checksum = "79c3c892f121fff406e5dd6b28c1b30096b95111c30701a899d4f2b18da6d1bd" dependencies = [ "displaydoc", "smallvec", @@ -1311,6 +1348,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "hybrid-array" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8655f91cd07f2b9d0c24137bd650fe69617773435ee5ec83022377777ce65ef1" +dependencies = [ + "typenum", +] + [[package]] name = "iana-time-zone" version = "0.1.65" @@ -1337,9 +1383,9 @@ dependencies = [ [[package]] name = "icu_calendar" -version = "2.1.1" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f0e52e009b6b16ba9c0693578796f2dd4aaa59a7f8f920423706714a89ac4e" +checksum = "a2b2acc6263f494f1df50685b53ff8e57869e47d5c6fe39c23d518ae9a4f3e45" dependencies = [ "calendrical_calculations", "displaydoc", @@ -1354,15 +1400,15 @@ dependencies = [ [[package]] name = "icu_calendar_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527f04223b17edfe0bd43baf14a0cb1b017830db65f3950dc00224860a9a446d" +checksum = "118577bcf3a0fa7c6ac0a7d6e951814da84ee56b9b1f68fb4d8d10b08cefaf4d" [[package]] name = "icu_collator" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32eed11a5572f1088b63fa21dc2e70d4a865e5739fc2d10abc05be93bae97019" +checksum = "b521b92a2666061ddda902769d8a4cf730b5c9529a845cc1b69770b12a6c9a71" dependencies = [ "icu_collator_data", "icu_collections", @@ -1379,18 +1425,19 @@ dependencies = [ [[package]] name = "icu_collator_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ab06f0e83a613efddba3e4913e00e43ed4001fae651cb7d40fc7e66b83b6fb9" +checksum = "038ed8e5817f2059c2f3efb0945ba78d060d3d25e8f1a1bea5139f821a21a2f0" [[package]] name = "icu_collections" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -1398,9 +1445,9 @@ dependencies = [ [[package]] name = "icu_datetime" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9d49f41ded8e63761b6b4c3120dfdc289415a1ed10107db6198eb311057ca5" +checksum = "989d56ea5bbc43ae2b4e0388874b002884eaf4ed3a76c84a6c8c5ad575e04d72" dependencies = [ "displaydoc", "fixed_decimal", @@ -1421,20 +1468,22 @@ dependencies = [ [[package]] name = "icu_datetime_data" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46597233625417b7c8052a63d916e4fdc73df21614ac0b679492a5d6e3b01aeb" +checksum = "40d3cc1b690d9703202bc319692ac8a1f3a6390686f0930ff40542450fa34f0b" [[package]] name = "icu_decimal" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a38c52231bc348f9b982c1868a2af3195199623007ba2c7650f432038f5b3e8e" +checksum = "288247df2e32aa776ac54fdd64de552149ac43cb840f2761811f0e8d09719dd4" dependencies = [ + "displaydoc", "fixed_decimal", "icu_decimal_data", "icu_locale", "icu_locale_core", + "icu_plurals", "icu_provider", "writeable", "zerovec", @@ -1442,15 +1491,15 @@ dependencies = [ [[package]] name = "icu_decimal_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2905b4044eab2dd848fe84199f9195567b63ab3a93094711501363f63546fef7" +checksum = "6f14a5ca9e8af29eef62064f269078424283d90dbaffeac5225addf62aaabc22" [[package]] name = "icu_locale" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "532b11722e350ab6bf916ba6eb0efe3ee54b932666afec989465f9243fe6dd60" +checksum = "d5a396343c7208121dc86e35623d3dfe19814a7613cfd14964994cdc9c9a2e26" dependencies = [ "icu_collections", "icu_locale_core", @@ -1463,9 +1512,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -1477,15 +1526,15 @@ dependencies = [ [[package]] name = "icu_locale_data" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c5f1d16b4c3a2642d3a719f18f6b06070ab0aef246a6418130c955ae08aa831" +checksum = "d5fdcc9ac77c6d74ff5cf6e65ef3181d6af32003b16fce3a77fb451d2f695993" [[package]] name = "icu_normalizer" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -1500,15 +1549,15 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_pattern" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a7ff8c0ff6f61cdce299dcb54f557b0a251adbc78f6f0c35a21332c452b4a1b" +checksum = "1c4c568054ffe735398a9f4c55aec37ad7c768844553cc0978f09cc9b933a1fb" dependencies = [ "displaydoc", "either", @@ -1519,9 +1568,9 @@ dependencies = [ [[package]] name = "icu_plurals" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f9cfe49f5b1d1163cc58db451562339916a9ca5cbcaae83924d41a0bf839474" +checksum = "2a50023f1d49ad5c4333380328a0d4a19e4b9d6d842ec06639affd5ba47c8103" dependencies = [ "fixed_decimal", "icu_locale", @@ -1532,15 +1581,15 @@ dependencies = [ [[package]] name = "icu_plurals_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f018a98dccf7f0eb02ba06ac0ff67d102d8ded80734724305e924de304e12ff0" +checksum = "8485497155dc865f901decb93ecc20d3e467df67bfeceb91e3ba34e2b11e8e1d" [[package]] name = "icu_properties" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ "icu_collections", "icu_locale_core", @@ -1552,15 +1601,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", @@ -1575,9 +1624,9 @@ dependencies = [ [[package]] name = "icu_time" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8242b00da3b3b6678f731437a11c8833a43c821ae081eca60ba1b7579d45b6d8" +checksum = "ec3af0c141da0a61d4f6970cd1d5f4b388b17ea22f8124f8f6049d3d5147586a" dependencies = [ "calendrical_calculations", "displaydoc", @@ -1592,9 +1641,9 @@ dependencies = [ [[package]] name = "icu_time_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e10b0e5e87a2c84bd5fa407705732052edebe69291d347d0c3033785470edbf" +checksum = "6f2f8aeca682d874a5247084aa4fb7d1cef9ba45d889c21209a8818dcaaa0ec9" [[package]] name = "id-arena" @@ -1692,9 +1741,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" @@ -1823,12 +1872,13 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.12" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08" dependencies = [ "bitflags 2.11.0", "libc", + "plain", "redox_syscall 0.7.0", ] @@ -1891,7 +1941,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ "cfg-if", - "digest", + "digest 0.10.7", ] [[package]] @@ -2244,14 +2294,20 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + [[package]] name = "platform-info" -version = "2.0.5" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7539aeb3fdd8cb4f6a331307cf71a1039cee75e94e8a71725b9484f4a0d9451a" +checksum = "9368d62437c8cbb7c31ee37fd8c08a7d390e09a3ff75698a674953f46705ffcb" dependencies = [ "libc", - "winapi", + "windows-sys 0.59.0", ] [[package]] @@ -2558,6 +2614,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "rstest_reuse" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3a8fb4672e840a587a66fc577a5491375df51ddb88f2a2c2a792598c326fe14" +dependencies = [ + "quote", + "rand 0.8.5", + "syn", +] + [[package]] name = "rust-ini" version = "0.21.3" @@ -2570,9 +2637,9 @@ dependencies = [ [[package]] name = "rustc-hash" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" [[package]] name = "rustc_version" @@ -2707,7 +2774,7 @@ checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures 0.2.17", - "digest", + "digest 0.10.7", ] [[package]] @@ -2718,7 +2785,7 @@ checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures 0.2.17", - "digest", + "digest 0.10.7", ] [[package]] @@ -2727,7 +2794,7 @@ version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" dependencies = [ - "digest", + "digest 0.10.7", "keccak", ] @@ -2788,11 +2855,11 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "sm3" -version = "0.4.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebb9a3b702d0a7e33bc4d85a14456633d2b165c2ad839c5fd9a8417c1ab15860" +checksum = "da6a89ba31723d185fd7413b98c576a575f356d9b84729d8ecb6ead60000a5b6" dependencies = [ - "digest", + "digest 0.11.2", ] [[package]] @@ -2898,12 +2965,12 @@ dependencies = [ [[package]] name = "terminal_size" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" +checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -3002,9 +3069,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "serde_core", @@ -3166,7 +3233,7 @@ dependencies = [ [[package]] name = "uu_arch" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3176,7 +3243,7 @@ dependencies = [ [[package]] name = "uu_b2sum" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3186,7 +3253,7 @@ dependencies = [ [[package]] name = "uu_base32" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3195,7 +3262,7 @@ dependencies = [ [[package]] name = "uu_base64" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3207,7 +3274,7 @@ dependencies = [ [[package]] name = "uu_basename" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3216,7 +3283,7 @@ dependencies = [ [[package]] name = "uu_basenc" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3226,12 +3293,13 @@ dependencies = [ [[package]] name = "uu_cat" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", + "codspeed-divan-compat", "fluent", "memchr", - "nix", + "rustix", "tempfile", "thiserror 2.0.18", "uucore", @@ -3241,7 +3309,7 @@ dependencies = [ [[package]] name = "uu_chcon" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3254,7 +3322,7 @@ dependencies = [ [[package]] name = "uu_checksum_common" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3263,7 +3331,7 @@ dependencies = [ [[package]] name = "uu_chgrp" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3272,7 +3340,7 @@ dependencies = [ [[package]] name = "uu_chmod" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3282,7 +3350,7 @@ dependencies = [ [[package]] name = "uu_chown" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3291,7 +3359,7 @@ dependencies = [ [[package]] name = "uu_chroot" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3301,7 +3369,7 @@ dependencies = [ [[package]] name = "uu_cksum" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3312,7 +3380,7 @@ dependencies = [ [[package]] name = "uu_comm" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3321,7 +3389,7 @@ dependencies = [ [[package]] name = "uu_cp" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3340,7 +3408,7 @@ dependencies = [ [[package]] name = "uu_csplit" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3353,7 +3421,7 @@ dependencies = [ [[package]] name = "uu_cut" -version = "0.7.0" +version = "0.8.0" dependencies = [ "bstr", "clap", @@ -3365,7 +3433,7 @@ dependencies = [ [[package]] name = "uu_date" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3374,9 +3442,10 @@ dependencies = [ "icu_locale", "jiff", "jiff-icu", - "nix", + "libc", "parse_datetime", "regex", + "rustix", "tempfile", "uucore", "windows-sys 0.61.2", @@ -3384,7 +3453,7 @@ dependencies = [ [[package]] name = "uu_dd" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3399,12 +3468,12 @@ dependencies = [ [[package]] name = "uu_df" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "codspeed-divan-compat", "fluent", - "nix", + "rustix", "tempfile", "thiserror 2.0.18", "unicode-width 0.2.2", @@ -3413,7 +3482,7 @@ dependencies = [ [[package]] name = "uu_dir" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "uu_ls", @@ -3422,7 +3491,7 @@ dependencies = [ [[package]] name = "uu_dircolors" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3431,7 +3500,7 @@ dependencies = [ [[package]] name = "uu_dirname" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3440,7 +3509,7 @@ dependencies = [ [[package]] name = "uu_du" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3455,7 +3524,7 @@ dependencies = [ [[package]] name = "uu_echo" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3464,7 +3533,7 @@ dependencies = [ [[package]] name = "uu_env" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3476,7 +3545,7 @@ dependencies = [ [[package]] name = "uu_expand" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3489,7 +3558,7 @@ dependencies = [ [[package]] name = "uu_expr" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3502,7 +3571,7 @@ dependencies = [ [[package]] name = "uu_factor" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3515,7 +3584,7 @@ dependencies = [ [[package]] name = "uu_false" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3525,7 +3594,7 @@ dependencies = [ [[package]] name = "uu_fmt" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3536,7 +3605,7 @@ dependencies = [ [[package]] name = "uu_fold" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3548,7 +3617,7 @@ dependencies = [ [[package]] name = "uu_groups" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3558,7 +3627,7 @@ dependencies = [ [[package]] name = "uu_head" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3569,7 +3638,7 @@ dependencies = [ [[package]] name = "uu_hostid" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3579,7 +3648,7 @@ dependencies = [ [[package]] name = "uu_hostname" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3593,7 +3662,7 @@ dependencies = [ [[package]] name = "uu_id" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3603,7 +3672,7 @@ dependencies = [ [[package]] name = "uu_install" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "file_diff", @@ -3616,7 +3685,7 @@ dependencies = [ [[package]] name = "uu_join" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3629,7 +3698,7 @@ dependencies = [ [[package]] name = "uu_kill" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3639,7 +3708,7 @@ dependencies = [ [[package]] name = "uu_link" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3648,7 +3717,7 @@ dependencies = [ [[package]] name = "uu_ln" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3658,7 +3727,7 @@ dependencies = [ [[package]] name = "uu_logname" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3668,7 +3737,7 @@ dependencies = [ [[package]] name = "uu_ls" -version = "0.7.0" +version = "0.8.0" dependencies = [ "ansi-width", "clap", @@ -3688,7 +3757,7 @@ dependencies = [ [[package]] name = "uu_md5sum" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3698,16 +3767,17 @@ dependencies = [ [[package]] name = "uu_mkdir" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", + "rustix", "uucore", ] [[package]] name = "uu_mkfifo" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3717,7 +3787,7 @@ dependencies = [ [[package]] name = "uu_mknod" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3727,7 +3797,7 @@ dependencies = [ [[package]] name = "uu_mktemp" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3739,7 +3809,7 @@ dependencies = [ [[package]] name = "uu_more" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "crossterm", @@ -3750,7 +3820,7 @@ dependencies = [ [[package]] name = "uu_mv" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3767,18 +3837,17 @@ dependencies = [ [[package]] name = "uu_nice" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", - "libc", - "nix", + "rustix", "uucore", ] [[package]] name = "uu_nl" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3791,7 +3860,7 @@ dependencies = [ [[package]] name = "uu_nohup" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3802,7 +3871,7 @@ dependencies = [ [[package]] name = "uu_nproc" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3812,7 +3881,7 @@ dependencies = [ [[package]] name = "uu_numfmt" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3824,7 +3893,7 @@ dependencies = [ [[package]] name = "uu_od" -version = "0.7.0" +version = "0.8.0" dependencies = [ "byteorder", "clap", @@ -3836,7 +3905,7 @@ dependencies = [ [[package]] name = "uu_paste" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3845,7 +3914,7 @@ dependencies = [ [[package]] name = "uu_pathchk" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3855,7 +3924,7 @@ dependencies = [ [[package]] name = "uu_pinky" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3864,7 +3933,7 @@ dependencies = [ [[package]] name = "uu_pr" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3877,7 +3946,7 @@ dependencies = [ [[package]] name = "uu_printenv" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3886,7 +3955,7 @@ dependencies = [ [[package]] name = "uu_printf" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3895,7 +3964,7 @@ dependencies = [ [[package]] name = "uu_ptx" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3906,7 +3975,7 @@ dependencies = [ [[package]] name = "uu_pwd" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3915,7 +3984,7 @@ dependencies = [ [[package]] name = "uu_readlink" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3924,7 +3993,7 @@ dependencies = [ [[package]] name = "uu_realpath" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3933,7 +4002,7 @@ dependencies = [ [[package]] name = "uu_rm" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3948,7 +4017,7 @@ dependencies = [ [[package]] name = "uu_rmdir" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3958,7 +4027,7 @@ dependencies = [ [[package]] name = "uu_runcon" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3970,7 +4039,7 @@ dependencies = [ [[package]] name = "uu_seq" -version = "0.7.0" +version = "0.8.0" dependencies = [ "bigdecimal", "clap", @@ -3984,7 +4053,7 @@ dependencies = [ [[package]] name = "uu_sha1sum" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -3994,7 +4063,7 @@ dependencies = [ [[package]] name = "uu_sha224sum" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -4004,7 +4073,7 @@ dependencies = [ [[package]] name = "uu_sha256sum" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -4014,7 +4083,7 @@ dependencies = [ [[package]] name = "uu_sha384sum" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -4024,7 +4093,7 @@ dependencies = [ [[package]] name = "uu_sha512sum" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -4034,7 +4103,7 @@ dependencies = [ [[package]] name = "uu_shred" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -4045,7 +4114,7 @@ dependencies = [ [[package]] name = "uu_shuf" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -4060,7 +4129,7 @@ dependencies = [ [[package]] name = "uu_sleep" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -4069,7 +4138,7 @@ dependencies = [ [[package]] name = "uu_sort" -version = "0.7.0" +version = "0.8.0" dependencies = [ "bigdecimal", "binary-heap-plus", @@ -4092,7 +4161,7 @@ dependencies = [ [[package]] name = "uu_split" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -4105,7 +4174,7 @@ dependencies = [ [[package]] name = "uu_stat" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -4115,7 +4184,7 @@ dependencies = [ [[package]] name = "uu_stdbuf" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -4127,7 +4196,7 @@ dependencies = [ [[package]] name = "uu_stdbuf_libstdbuf" -version = "0.7.0" +version = "0.8.0" dependencies = [ "ctor", "libc", @@ -4135,7 +4204,7 @@ dependencies = [ [[package]] name = "uu_stty" -version = "0.7.0" +version = "0.8.0" dependencies = [ "cfg_aliases", "clap", @@ -4146,7 +4215,7 @@ dependencies = [ [[package]] name = "uu_sum" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -4155,7 +4224,7 @@ dependencies = [ [[package]] name = "uu_sync" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -4166,7 +4235,7 @@ dependencies = [ [[package]] name = "uu_tac" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -4181,15 +4250,15 @@ dependencies = [ [[package]] name = "uu_tail" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", "libc", "memchr", - "nix", "notify", "rstest", + "rustix", "same-file", "uucore", "windows-sys 0.61.2", @@ -4197,7 +4266,7 @@ dependencies = [ [[package]] name = "uu_tee" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -4206,7 +4275,7 @@ dependencies = [ [[package]] name = "uu_test" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -4218,9 +4287,10 @@ dependencies = [ [[package]] name = "uu_timeout" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", + "codspeed-divan-compat", "fluent", "libc", "nix", @@ -4229,14 +4299,15 @@ dependencies = [ [[package]] name = "uu_touch" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "filetime", "fluent", "jiff", - "nix", + "libc", "parse_datetime", + "rustix", "tempfile", "thiserror 2.0.18", "uucore", @@ -4245,7 +4316,7 @@ dependencies = [ [[package]] name = "uu_tr" -version = "0.7.0" +version = "0.8.0" dependencies = [ "bytecount", "clap", @@ -4256,7 +4327,7 @@ dependencies = [ [[package]] name = "uu_true" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -4266,7 +4337,7 @@ dependencies = [ [[package]] name = "uu_truncate" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -4275,13 +4346,13 @@ dependencies = [ [[package]] name = "uu_tsort" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "codspeed-divan-compat", "fluent", - "nix", "rustc-hash", + "rustix", "string-interner", "thiserror 2.0.18", "uucore", @@ -4289,17 +4360,17 @@ dependencies = [ [[package]] name = "uu_tty" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", - "nix", + "rustix", "uucore", ] [[package]] name = "uu_uname" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -4309,7 +4380,7 @@ dependencies = [ [[package]] name = "uu_unexpand" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -4321,7 +4392,7 @@ dependencies = [ [[package]] name = "uu_uniq" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -4331,7 +4402,7 @@ dependencies = [ [[package]] name = "uu_unlink" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -4340,7 +4411,7 @@ dependencies = [ [[package]] name = "uu_uptime" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -4351,7 +4422,7 @@ dependencies = [ [[package]] name = "uu_users" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -4361,7 +4432,7 @@ dependencies = [ [[package]] name = "uu_vdir" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "uu_ls", @@ -4370,14 +4441,14 @@ dependencies = [ [[package]] name = "uu_wc" -version = "0.7.0" +version = "0.8.0" dependencies = [ "bytecount", "clap", "codspeed-divan-compat", "fluent", "libc", - "nix", + "rustix", "tempfile", "thiserror 2.0.18", "unicode-width 0.2.2", @@ -4386,7 +4457,7 @@ dependencies = [ [[package]] name = "uu_who" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -4395,7 +4466,7 @@ dependencies = [ [[package]] name = "uu_whoami" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -4405,7 +4476,7 @@ dependencies = [ [[package]] name = "uu_yes" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -4415,7 +4486,7 @@ dependencies = [ [[package]] name = "uucore" -version = "0.7.0" +version = "0.8.0" dependencies = [ "base64-simd", "bigdecimal", @@ -4427,7 +4498,7 @@ dependencies = [ "crc-fast", "data-encoding", "data-encoding-macro", - "digest", + "digest 0.10.7", "dns-lookup", "dunce", "fluent", @@ -4452,6 +4523,7 @@ dependencies = [ "os_display", "procfs", "rustc-hash", + "rustix", "selinux", "sha1", "sha2", @@ -4474,7 +4546,7 @@ dependencies = [ [[package]] name = "uucore_procs" -version = "0.7.0" +version = "0.8.0" dependencies = [ "proc-macro2", "quote", @@ -4492,7 +4564,7 @@ dependencies = [ [[package]] name = "uutests" -version = "0.7.0" +version = "0.8.0" dependencies = [ "ctor", "libc", @@ -4508,9 +4580,9 @@ dependencies = [ [[package]] name = "uutils_term_grid" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcba141ce511bad08e80b43f02976571072e1ff4286f7d628943efbd277c6361" +checksum = "382d49b39de4a115f203305057741126b09a615892d773a2d419a2b816e30e39" dependencies = [ "ansi-width", ] @@ -5044,9 +5116,9 @@ checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" [[package]] name = "yoke" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -5055,9 +5127,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", @@ -5135,20 +5207,21 @@ dependencies = [ [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", "zerofrom", + "zerovec", ] [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "serde", "yoke", @@ -5158,9 +5231,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", @@ -5169,9 +5242,9 @@ dependencies = [ [[package]] name = "zip" -version = "8.2.0" +version = "8.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b680f2a0cd479b4cff6e1233c483fdead418106eae419dc60200ae9850f6d004" +checksum = "2726508a48f38dceb22b35ecbbd2430efe34ff05c62bd3285f965d7911b33464" dependencies = [ "crc32fast", "flate2", diff --git a/Cargo.toml b/Cargo.toml index 92013b0af57..d27547b6760 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ # coreutils (uutils) # * see the repository LICENSE, README, and CONTRIBUTING files for more information -# spell-checker:ignore (libs) bigdecimal datetime foldhash serde gethostid kqueue libselinux mangen memmap uuhelp startswith constness expl unnested logind cfgs interner +# spell-checker:ignore (libs) bigdecimal datetime foldhash serde gethostid kqueue libselinux mangen memmap uuhelp startswith constness expl unnested logind cfgs interner getauxval [package] name = "coreutils" @@ -23,7 +23,6 @@ all-features = true [features] default = ["feat_common_core"] ## OS feature shortcodes -macos = ["feat_os_macos"] unix = ["feat_os_unix"] windows = ["feat_os_windows"] ## project-specific feature shortcodes @@ -132,6 +131,7 @@ feat_common_core = [ "numfmt", "od", "paste", + "pathchk", "pr", "printenv", "printf", @@ -179,12 +179,19 @@ feat_Tier1 = [ # We don't need to support all of wasm targets. So the ambiguous name is used at here # It is bit complex to deduplicate with other lists feat_wasm = [ - "basename", + "arch", "base32", "base64", "basenc", + "basename", + "cat", + "comm", + "cp", + "csplit", "cut", + "ls", "date", + "dir", "dircolors", "dirname", "echo", @@ -193,28 +200,49 @@ feat_wasm = [ "false", "fmt", "fold", + "head", "join", "link", + "ln", + "ls", + "mkdir", + "mv", "nl", + "nproc", "numfmt", "od", "paste", + "pathchk", "pr", "printenv", + "head", "printf", "ptx", "pwd", + "readlink", + "realpath", + "rm", + "rmdir", "seq", + "sort", + "split", "shred", "shuf", "sleep", + "sort", "sum", + "tail", "tee", + "touch", + "tr", "true", "truncate", + "tsort", + "uname", "unexpand", "uniq", "unlink", + "vdir", "wc", "yes", # cksum family @@ -226,13 +254,6 @@ feat_wasm = [ "sha256sum", "sha384sum", "sha512sum", - # useless? - "arch", - "uname", -] -# "feat_os_macos" == set of utilities which can be built/run on the MacOS platform -feat_os_macos = [ - "feat_os_unix", ## == a modern/usual *nix platform ] # "feat_os_unix" == set of utilities which can be built/run on modern/usual *nix platforms. feat_os_unix = [ @@ -292,7 +313,6 @@ feat_require_unix_core = [ "mknod", "nice", "nohup", - "pathchk", "stat", "stty", "timeout", @@ -321,7 +341,6 @@ feat_os_unix_fuchsia = [ "mkfifo", "mknod", "nice", - "pathchk", "tty", "uname", "unlink", @@ -368,7 +387,7 @@ homepage = "https://github.com/uutils/coreutils" keywords = ["coreutils", "uutils", "cross-platform", "cli", "utility"] license = "MIT" readme = "README.package.md" -version = "0.7.0" +version = "0.8.0" [workspace.dependencies] ansi-width = "0.1.0" @@ -379,10 +398,10 @@ bytecount = "0.6.8" byteorder = "1.5.0" clap = { version = "4.5", features = ["wrap_help", "cargo", "color"] } clap_complete = "4.4" -clap_mangen = "0.2" +clap_mangen = "0.3" compare = "0.1.0" crossterm = { version = "0.29.0", default-features = false } -ctor = "0.6.0" +ctor = "0.8.0" ctrlc = { version = "3.5.2", features = ["termination"] } divan = { package = "codspeed-divan-compat", version = "4.4.1" } dns-lookup = { version = "3.0.0" } @@ -432,8 +451,12 @@ rayon = "1.10" regex = "1.10.4" rlimit = "0.11.0" rstest = "0.26.0" +rstest_reuse = "0.7.0" rustc-hash = "2.1.1" rust-ini = "0.21.0" +# binary name of coreutils can be hijacked by overriding getauxval via LD_PRELOAD +# So we use param and avoid libc backend +rustix = { version = "1.1.4", features = ["param"] } same-file = "1.0.6" self_cell = "1.0.4" selinux = "=0.6.0" @@ -446,7 +469,7 @@ time = { version = "0.3.36" } unicode-width = "0.2.0" unit-prefix = "0.5" utmp-classic = "0.1.6" -uutils_term_grid = "0.7" +uutils_term_grid = "0.8" walkdir = "2.5" winapi-util = "0.1.8" windows-sys = { version = "0.61.0", default-features = false } @@ -460,7 +483,7 @@ sha2 = "0.10.8" sha3 = "0.10.8" blake2b_simd = "1.0.2" blake3 = "1.5.1" -sm3 = "0.4.2" +sm3 = "0.5.0" crc-fast = { version = "1.5.0", default-features = false } digest = "0.10.7" @@ -470,12 +493,12 @@ fluent-bundle = "0.16.0" unic-langid = "0.9.6" fluent-syntax = "0.12.0" -uucore = { version = "0.7.0", package = "uucore", path = "src/uucore" } -uucore_procs = { version = "0.7.0", package = "uucore_procs", path = "src/uucore_procs" } -uu_ls = { version = "0.7.0", path = "src/uu/ls" } -uu_base32 = { version = "0.7.0", path = "src/uu/base32" } -uu_checksum_common = { version = "0.7.0", path = "src/uu/checksum_common" } -uutests = { version = "0.7.0", package = "uutests", path = "tests/uutests" } +uucore = { version = "0.8.0", package = "uucore", path = "src/uucore" } +uucore_procs = { version = "0.8.0", package = "uucore_procs", path = "src/uucore_procs" } +uu_ls = { version = "0.8.0", path = "src/uu/ls" } +uu_base32 = { version = "0.8.0", path = "src/uu/base32" } +uu_checksum_common = { version = "0.8.0", path = "src/uu/checksum_common" } +uutests = { version = "0.8.0", package = "uutests", path = "tests/uutests" } [dependencies] clap.workspace = true @@ -493,115 +516,118 @@ zip = { workspace = true, optional = true } # * uutils -uu_test = { optional = true, version = "0.7.0", package = "uu_test", path = "src/uu/test" } +uu_test = { optional = true, version = "0.8.0", package = "uu_test", path = "src/uu/test" } # -arch = { optional = true, version = "0.7.0", package = "uu_arch", path = "src/uu/arch" } -base32 = { optional = true, version = "0.7.0", package = "uu_base32", path = "src/uu/base32" } -base64 = { optional = true, version = "0.7.0", package = "uu_base64", path = "src/uu/base64" } -basename = { optional = true, version = "0.7.0", package = "uu_basename", path = "src/uu/basename" } -basenc = { optional = true, version = "0.7.0", package = "uu_basenc", path = "src/uu/basenc" } -cat = { optional = true, version = "0.7.0", package = "uu_cat", path = "src/uu/cat" } -chcon = { optional = true, version = "0.7.0", package = "uu_chcon", path = "src/uu/chcon" } -chgrp = { optional = true, version = "0.7.0", package = "uu_chgrp", path = "src/uu/chgrp" } -chmod = { optional = true, version = "0.7.0", package = "uu_chmod", path = "src/uu/chmod" } -chown = { optional = true, version = "0.7.0", package = "uu_chown", path = "src/uu/chown" } -chroot = { optional = true, version = "0.7.0", package = "uu_chroot", path = "src/uu/chroot" } -cksum = { optional = true, version = "0.7.0", package = "uu_cksum", path = "src/uu/cksum" } -b2sum = { optional = true, version = "0.7.0", package = "uu_b2sum", path = "src/uu/b2sum" } -md5sum = { optional = true, version = "0.7.0", package = "uu_md5sum", path = "src/uu/md5sum" } -sha1sum = { optional = true, version = "0.7.0", package = "uu_sha1sum", path = "src/uu/sha1sum" } -sha224sum = { optional = true, version = "0.7.0", package = "uu_sha224sum", path = "src/uu/sha224sum" } -sha256sum = { optional = true, version = "0.7.0", package = "uu_sha256sum", path = "src/uu/sha256sum" } -sha384sum = { optional = true, version = "0.7.0", package = "uu_sha384sum", path = "src/uu/sha384sum" } -sha512sum = { optional = true, version = "0.7.0", package = "uu_sha512sum", path = "src/uu/sha512sum" } -comm = { optional = true, version = "0.7.0", package = "uu_comm", path = "src/uu/comm" } -cp = { optional = true, version = "0.7.0", package = "uu_cp", path = "src/uu/cp" } -csplit = { optional = true, version = "0.7.0", package = "uu_csplit", path = "src/uu/csplit" } -cut = { optional = true, version = "0.7.0", package = "uu_cut", path = "src/uu/cut" } -date = { optional = true, version = "0.7.0", package = "uu_date", path = "src/uu/date" } -dd = { optional = true, version = "0.7.0", package = "uu_dd", path = "src/uu/dd" } -df = { optional = true, version = "0.7.0", package = "uu_df", path = "src/uu/df" } -dir = { optional = true, version = "0.7.0", package = "uu_dir", path = "src/uu/dir" } -dircolors = { optional = true, version = "0.7.0", package = "uu_dircolors", path = "src/uu/dircolors" } -dirname = { optional = true, version = "0.7.0", package = "uu_dirname", path = "src/uu/dirname" } -du = { optional = true, version = "0.7.0", package = "uu_du", path = "src/uu/du" } -echo = { optional = true, version = "0.7.0", package = "uu_echo", path = "src/uu/echo" } -env = { optional = true, version = "0.7.0", package = "uu_env", path = "src/uu/env" } -expand = { optional = true, version = "0.7.0", package = "uu_expand", path = "src/uu/expand" } -expr = { optional = true, version = "0.7.0", package = "uu_expr", path = "src/uu/expr" } -factor = { optional = true, version = "0.7.0", package = "uu_factor", path = "src/uu/factor" } -false = { optional = true, version = "0.7.0", package = "uu_false", path = "src/uu/false" } -fmt = { optional = true, version = "0.7.0", package = "uu_fmt", path = "src/uu/fmt" } -fold = { optional = true, version = "0.7.0", package = "uu_fold", path = "src/uu/fold" } -groups = { optional = true, version = "0.7.0", package = "uu_groups", path = "src/uu/groups" } -head = { optional = true, version = "0.7.0", package = "uu_head", path = "src/uu/head" } -hostid = { optional = true, version = "0.7.0", package = "uu_hostid", path = "src/uu/hostid" } -hostname = { optional = true, version = "0.7.0", package = "uu_hostname", path = "src/uu/hostname" } -id = { optional = true, version = "0.7.0", package = "uu_id", path = "src/uu/id" } -install = { optional = true, version = "0.7.0", package = "uu_install", path = "src/uu/install" } -join = { optional = true, version = "0.7.0", package = "uu_join", path = "src/uu/join" } -kill = { optional = true, version = "0.7.0", package = "uu_kill", path = "src/uu/kill" } -link = { optional = true, version = "0.7.0", package = "uu_link", path = "src/uu/link" } -ln = { optional = true, version = "0.7.0", package = "uu_ln", path = "src/uu/ln" } -ls = { optional = true, version = "0.7.0", package = "uu_ls", path = "src/uu/ls" } -logname = { optional = true, version = "0.7.0", package = "uu_logname", path = "src/uu/logname" } -mkdir = { optional = true, version = "0.7.0", package = "uu_mkdir", path = "src/uu/mkdir" } -mkfifo = { optional = true, version = "0.7.0", package = "uu_mkfifo", path = "src/uu/mkfifo" } -mknod = { optional = true, version = "0.7.0", package = "uu_mknod", path = "src/uu/mknod" } -mktemp = { optional = true, version = "0.7.0", package = "uu_mktemp", path = "src/uu/mktemp" } -more = { optional = true, version = "0.7.0", package = "uu_more", path = "src/uu/more" } -mv = { optional = true, version = "0.7.0", package = "uu_mv", path = "src/uu/mv" } -nice = { optional = true, version = "0.7.0", package = "uu_nice", path = "src/uu/nice" } -nl = { optional = true, version = "0.7.0", package = "uu_nl", path = "src/uu/nl" } -nohup = { optional = true, version = "0.7.0", package = "uu_nohup", path = "src/uu/nohup" } -nproc = { optional = true, version = "0.7.0", package = "uu_nproc", path = "src/uu/nproc" } -numfmt = { optional = true, version = "0.7.0", package = "uu_numfmt", path = "src/uu/numfmt" } -od = { optional = true, version = "0.7.0", package = "uu_od", path = "src/uu/od" } -paste = { optional = true, version = "0.7.0", package = "uu_paste", path = "src/uu/paste" } -pathchk = { optional = true, version = "0.7.0", package = "uu_pathchk", path = "src/uu/pathchk" } -pinky = { optional = true, version = "0.7.0", package = "uu_pinky", path = "src/uu/pinky" } -pr = { optional = true, version = "0.7.0", package = "uu_pr", path = "src/uu/pr" } -printenv = { optional = true, version = "0.7.0", package = "uu_printenv", path = "src/uu/printenv" } -printf = { optional = true, version = "0.7.0", package = "uu_printf", path = "src/uu/printf" } -ptx = { optional = true, version = "0.7.0", package = "uu_ptx", path = "src/uu/ptx" } -pwd = { optional = true, version = "0.7.0", package = "uu_pwd", path = "src/uu/pwd" } -readlink = { optional = true, version = "0.7.0", package = "uu_readlink", path = "src/uu/readlink" } -realpath = { optional = true, version = "0.7.0", package = "uu_realpath", path = "src/uu/realpath" } -rm = { optional = true, version = "0.7.0", package = "uu_rm", path = "src/uu/rm" } -rmdir = { optional = true, version = "0.7.0", package = "uu_rmdir", path = "src/uu/rmdir" } -runcon = { optional = true, version = "0.7.0", package = "uu_runcon", path = "src/uu/runcon" } -seq = { optional = true, version = "0.7.0", package = "uu_seq", path = "src/uu/seq" } -shred = { optional = true, version = "0.7.0", package = "uu_shred", path = "src/uu/shred" } -shuf = { optional = true, version = "0.7.0", package = "uu_shuf", path = "src/uu/shuf" } -sleep = { optional = true, version = "0.7.0", package = "uu_sleep", path = "src/uu/sleep" } -sort = { optional = true, version = "0.7.0", package = "uu_sort", path = "src/uu/sort" } -split = { optional = true, version = "0.7.0", package = "uu_split", path = "src/uu/split" } -stat = { optional = true, version = "0.7.0", package = "uu_stat", path = "src/uu/stat" } -stdbuf = { optional = true, version = "0.7.0", package = "uu_stdbuf", path = "src/uu/stdbuf" } -stty = { optional = true, version = "0.7.0", package = "uu_stty", path = "src/uu/stty" } -sum = { optional = true, version = "0.7.0", package = "uu_sum", path = "src/uu/sum" } -sync = { optional = true, version = "0.7.0", package = "uu_sync", path = "src/uu/sync" } -tac = { optional = true, version = "0.7.0", package = "uu_tac", path = "src/uu/tac" } -tail = { optional = true, version = "0.7.0", package = "uu_tail", path = "src/uu/tail" } -tee = { optional = true, version = "0.7.0", package = "uu_tee", path = "src/uu/tee" } -timeout = { optional = true, version = "0.7.0", package = "uu_timeout", path = "src/uu/timeout" } -touch = { optional = true, version = "0.7.0", package = "uu_touch", path = "src/uu/touch" } -tr = { optional = true, version = "0.7.0", package = "uu_tr", path = "src/uu/tr" } -true = { optional = true, version = "0.7.0", package = "uu_true", path = "src/uu/true" } -truncate = { optional = true, version = "0.7.0", package = "uu_truncate", path = "src/uu/truncate" } -tsort = { optional = true, version = "0.7.0", package = "uu_tsort", path = "src/uu/tsort" } -tty = { optional = true, version = "0.7.0", package = "uu_tty", path = "src/uu/tty" } -uname = { optional = true, version = "0.7.0", package = "uu_uname", path = "src/uu/uname" } -unexpand = { optional = true, version = "0.7.0", package = "uu_unexpand", path = "src/uu/unexpand" } -uniq = { optional = true, version = "0.7.0", package = "uu_uniq", path = "src/uu/uniq" } -unlink = { optional = true, version = "0.7.0", package = "uu_unlink", path = "src/uu/unlink" } -uptime = { optional = true, version = "0.7.0", package = "uu_uptime", path = "src/uu/uptime" } -users = { optional = true, version = "0.7.0", package = "uu_users", path = "src/uu/users" } -vdir = { optional = true, version = "0.7.0", package = "uu_vdir", path = "src/uu/vdir" } -wc = { optional = true, version = "0.7.0", package = "uu_wc", path = "src/uu/wc" } -who = { optional = true, version = "0.7.0", package = "uu_who", path = "src/uu/who" } -whoami = { optional = true, version = "0.7.0", package = "uu_whoami", path = "src/uu/whoami" } -yes = { optional = true, version = "0.7.0", package = "uu_yes", path = "src/uu/yes" } +arch = { optional = true, version = "0.8.0", package = "uu_arch", path = "src/uu/arch" } +base32 = { optional = true, version = "0.8.0", package = "uu_base32", path = "src/uu/base32" } +base64 = { optional = true, version = "0.8.0", package = "uu_base64", path = "src/uu/base64" } +basename = { optional = true, version = "0.8.0", package = "uu_basename", path = "src/uu/basename" } +basenc = { optional = true, version = "0.8.0", package = "uu_basenc", path = "src/uu/basenc" } +cat = { optional = true, version = "0.8.0", package = "uu_cat", path = "src/uu/cat" } +chcon = { optional = true, version = "0.8.0", package = "uu_chcon", path = "src/uu/chcon" } +chgrp = { optional = true, version = "0.8.0", package = "uu_chgrp", path = "src/uu/chgrp" } +chmod = { optional = true, version = "0.8.0", package = "uu_chmod", path = "src/uu/chmod" } +chown = { optional = true, version = "0.8.0", package = "uu_chown", path = "src/uu/chown" } +chroot = { optional = true, version = "0.8.0", package = "uu_chroot", path = "src/uu/chroot" } +cksum = { optional = true, version = "0.8.0", package = "uu_cksum", path = "src/uu/cksum" } +b2sum = { optional = true, version = "0.8.0", package = "uu_b2sum", path = "src/uu/b2sum" } +md5sum = { optional = true, version = "0.8.0", package = "uu_md5sum", path = "src/uu/md5sum" } +sha1sum = { optional = true, version = "0.8.0", package = "uu_sha1sum", path = "src/uu/sha1sum" } +sha224sum = { optional = true, version = "0.8.0", package = "uu_sha224sum", path = "src/uu/sha224sum" } +sha256sum = { optional = true, version = "0.8.0", package = "uu_sha256sum", path = "src/uu/sha256sum" } +sha384sum = { optional = true, version = "0.8.0", package = "uu_sha384sum", path = "src/uu/sha384sum" } +sha512sum = { optional = true, version = "0.8.0", package = "uu_sha512sum", path = "src/uu/sha512sum" } +comm = { optional = true, version = "0.8.0", package = "uu_comm", path = "src/uu/comm" } +cp = { optional = true, version = "0.8.0", package = "uu_cp", path = "src/uu/cp" } +csplit = { optional = true, version = "0.8.0", package = "uu_csplit", path = "src/uu/csplit" } +cut = { optional = true, version = "0.8.0", package = "uu_cut", path = "src/uu/cut" } +date = { optional = true, version = "0.8.0", package = "uu_date", path = "src/uu/date" } +dd = { optional = true, version = "0.8.0", package = "uu_dd", path = "src/uu/dd" } +df = { optional = true, version = "0.8.0", package = "uu_df", path = "src/uu/df" } +dir = { optional = true, version = "0.8.0", package = "uu_dir", path = "src/uu/dir" } +dircolors = { optional = true, version = "0.8.0", package = "uu_dircolors", path = "src/uu/dircolors" } +dirname = { optional = true, version = "0.8.0", package = "uu_dirname", path = "src/uu/dirname" } +du = { optional = true, version = "0.8.0", package = "uu_du", path = "src/uu/du" } +echo = { optional = true, version = "0.8.0", package = "uu_echo", path = "src/uu/echo" } +env = { optional = true, version = "0.8.0", package = "uu_env", path = "src/uu/env" } +expand = { optional = true, version = "0.8.0", package = "uu_expand", path = "src/uu/expand" } +expr = { optional = true, version = "0.8.0", package = "uu_expr", path = "src/uu/expr" } +factor = { optional = true, version = "0.8.0", package = "uu_factor", path = "src/uu/factor" } +false = { optional = true, version = "0.8.0", package = "uu_false", path = "src/uu/false" } +fmt = { optional = true, version = "0.8.0", package = "uu_fmt", path = "src/uu/fmt" } +fold = { optional = true, version = "0.8.0", package = "uu_fold", path = "src/uu/fold" } +groups = { optional = true, version = "0.8.0", package = "uu_groups", path = "src/uu/groups" } +head = { optional = true, version = "0.8.0", package = "uu_head", path = "src/uu/head" } +hostid = { optional = true, version = "0.8.0", package = "uu_hostid", path = "src/uu/hostid" } +hostname = { optional = true, version = "0.8.0", package = "uu_hostname", path = "src/uu/hostname" } +id = { optional = true, version = "0.8.0", package = "uu_id", path = "src/uu/id" } +install = { optional = true, version = "0.8.0", package = "uu_install", path = "src/uu/install" } +join = { optional = true, version = "0.8.0", package = "uu_join", path = "src/uu/join" } +kill = { optional = true, version = "0.8.0", package = "uu_kill", path = "src/uu/kill" } +link = { optional = true, version = "0.8.0", package = "uu_link", path = "src/uu/link" } +ln = { optional = true, version = "0.8.0", package = "uu_ln", path = "src/uu/ln" } +ls = { optional = true, version = "0.8.0", package = "uu_ls", path = "src/uu/ls" } +logname = { optional = true, version = "0.8.0", package = "uu_logname", path = "src/uu/logname" } +mkdir = { optional = true, version = "0.8.0", package = "uu_mkdir", path = "src/uu/mkdir" } +mkfifo = { optional = true, version = "0.8.0", package = "uu_mkfifo", path = "src/uu/mkfifo" } +mknod = { optional = true, version = "0.8.0", package = "uu_mknod", path = "src/uu/mknod" } +mktemp = { optional = true, version = "0.8.0", package = "uu_mktemp", path = "src/uu/mktemp" } +more = { optional = true, version = "0.8.0", package = "uu_more", path = "src/uu/more" } +mv = { optional = true, version = "0.8.0", package = "uu_mv", path = "src/uu/mv" } +nice = { optional = true, version = "0.8.0", package = "uu_nice", path = "src/uu/nice" } +nl = { optional = true, version = "0.8.0", package = "uu_nl", path = "src/uu/nl" } +nohup = { optional = true, version = "0.8.0", package = "uu_nohup", path = "src/uu/nohup" } +nproc = { optional = true, version = "0.8.0", package = "uu_nproc", path = "src/uu/nproc" } +numfmt = { optional = true, version = "0.8.0", package = "uu_numfmt", path = "src/uu/numfmt" } +od = { optional = true, version = "0.8.0", package = "uu_od", path = "src/uu/od" } +paste = { optional = true, version = "0.8.0", package = "uu_paste", path = "src/uu/paste" } +pathchk = { optional = true, version = "0.8.0", package = "uu_pathchk", path = "src/uu/pathchk" } +pinky = { optional = true, version = "0.8.0", package = "uu_pinky", path = "src/uu/pinky" } +pr = { optional = true, version = "0.8.0", package = "uu_pr", path = "src/uu/pr" } +printenv = { optional = true, version = "0.8.0", package = "uu_printenv", path = "src/uu/printenv" } +printf = { optional = true, version = "0.8.0", package = "uu_printf", path = "src/uu/printf" } +ptx = { optional = true, version = "0.8.0", package = "uu_ptx", path = "src/uu/ptx" } +pwd = { optional = true, version = "0.8.0", package = "uu_pwd", path = "src/uu/pwd" } +readlink = { optional = true, version = "0.8.0", package = "uu_readlink", path = "src/uu/readlink" } +realpath = { optional = true, version = "0.8.0", package = "uu_realpath", path = "src/uu/realpath" } +rm = { optional = true, version = "0.8.0", package = "uu_rm", path = "src/uu/rm" } +rmdir = { optional = true, version = "0.8.0", package = "uu_rmdir", path = "src/uu/rmdir" } +runcon = { optional = true, version = "0.8.0", package = "uu_runcon", path = "src/uu/runcon" } +seq = { optional = true, version = "0.8.0", package = "uu_seq", path = "src/uu/seq" } +shred = { optional = true, version = "0.8.0", package = "uu_shred", path = "src/uu/shred" } +shuf = { optional = true, version = "0.8.0", package = "uu_shuf", path = "src/uu/shuf" } +sleep = { optional = true, version = "0.8.0", package = "uu_sleep", path = "src/uu/sleep" } +sort = { optional = true, version = "0.8.0", package = "uu_sort", path = "src/uu/sort" } +split = { optional = true, version = "0.8.0", package = "uu_split", path = "src/uu/split" } +stat = { optional = true, version = "0.8.0", package = "uu_stat", path = "src/uu/stat" } +stdbuf = { optional = true, version = "0.8.0", package = "uu_stdbuf", path = "src/uu/stdbuf" } +stty = { optional = true, version = "0.8.0", package = "uu_stty", path = "src/uu/stty" } +sum = { optional = true, version = "0.8.0", package = "uu_sum", path = "src/uu/sum" } +sync = { optional = true, version = "0.8.0", package = "uu_sync", path = "src/uu/sync" } +tac = { optional = true, version = "0.8.0", package = "uu_tac", path = "src/uu/tac" } +tail = { optional = true, version = "0.8.0", package = "uu_tail", path = "src/uu/tail" } +tee = { optional = true, version = "0.8.0", package = "uu_tee", path = "src/uu/tee" } +timeout = { optional = true, version = "0.8.0", package = "uu_timeout", path = "src/uu/timeout" } +touch = { optional = true, version = "0.8.0", package = "uu_touch", path = "src/uu/touch" } +tr = { optional = true, version = "0.8.0", package = "uu_tr", path = "src/uu/tr" } +true = { optional = true, version = "0.8.0", package = "uu_true", path = "src/uu/true" } +truncate = { optional = true, version = "0.8.0", package = "uu_truncate", path = "src/uu/truncate" } +tsort = { optional = true, version = "0.8.0", package = "uu_tsort", path = "src/uu/tsort" } +tty = { optional = true, version = "0.8.0", package = "uu_tty", path = "src/uu/tty" } +uname = { optional = true, version = "0.8.0", package = "uu_uname", path = "src/uu/uname" } +unexpand = { optional = true, version = "0.8.0", package = "uu_unexpand", path = "src/uu/unexpand" } +uniq = { optional = true, version = "0.8.0", package = "uu_uniq", path = "src/uu/uniq" } +unlink = { optional = true, version = "0.8.0", package = "uu_unlink", path = "src/uu/unlink" } +uptime = { optional = true, version = "0.8.0", package = "uu_uptime", path = "src/uu/uptime" } +users = { optional = true, version = "0.8.0", package = "uu_users", path = "src/uu/users" } +vdir = { optional = true, version = "0.8.0", package = "uu_vdir", path = "src/uu/vdir" } +wc = { optional = true, version = "0.8.0", package = "uu_wc", path = "src/uu/wc" } +who = { optional = true, version = "0.8.0", package = "uu_who", path = "src/uu/who" } +whoami = { optional = true, version = "0.8.0", package = "uu_whoami", path = "src/uu/whoami" } +yes = { optional = true, version = "0.8.0", package = "uu_yes", path = "src/uu/yes" } + +[target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies] +rustix.workspace = true # 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" } @@ -640,6 +666,7 @@ uucore = { workspace = true, features = [ walkdir.workspace = true hex-literal = "1.0.0" rstest.workspace = true +rstest_reuse.workspace = true [target.'cfg(unix)'.dev-dependencies] nix = { workspace = true, features = [ diff --git a/GNUmakefile b/GNUmakefile index ae266b6c277..7a00f4121c6 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -62,34 +62,15 @@ TOYBOX_ROOT := $(BASEDIR)/tmp TOYBOX_VER := 0.8.12 TOYBOX_SRC := $(TOYBOX_ROOT)/toybox-$(TOYBOX_VER) -#------------------------------------------------------------------------ -# Detect the host system. -# On Windows uname -s might return MINGW_NT-* or CYGWIN_NT-*. -# Otherwise let it default to the kernel name returned by uname -s -# (Linux, Darwin, FreeBSD, …). -#------------------------------------------------------------------------ -OS ?= $(shell uname -s) - -# Windows does not allow symlink by default. -# Allow to override LN for AppArmor. -ifneq (,$(findstring _NT,$(OS))) - LN ?= ln -f -endif -LN ?= ln -sf - -# Possible programs -PROGS := \ - $(shell sed -n '/feat_Tier1 = \[/,/\]/p' Cargo.toml | sed '1d;2d' |tr -d '],"\n')\ - $(shell sed -n '/feat_common_core = \[/,/\]/p' Cargo.toml | sed '1d' |tr -d '],"\n') - -UNIX_PROGS := \ - $(shell sed -n '/feat_require_unix_core = \[/,/\]/p' Cargo.toml | sed '1d' |tr -d '],"\n') \ - hostid \ - pinky \ - stdbuf \ - uptime \ - users \ - who +# Detect the target system +# See https://doc.rust-lang.org/beta/rustc/platform-support.html +# todo: support building wasm +OS := $(or $(CARGO_BUILD_TARGET),$(shell rustc --print host-tuple)) + +# hardlinks are better default since +# - Windows(cygwin) does not allow symlink by default +# - std::env:current_exe resolves symlink +LN ?= ln -f SELINUX_PROGS := \ chcon \ @@ -97,9 +78,13 @@ SELINUX_PROGS := \ $(info Detected OS = $(OS)) -ifeq (,$(findstring MINGW,$(OS))) - PROGS += $(UNIX_PROGS) +ifeq (,$(findstring windows,$(OS))) + FEATURE_EXTRACT_UTILS := feat_os_unix +else + FEATURE_EXTRACT_UTILS := feat_Tier1 endif +PROGS := $(shell cargo tree --depth 1 --features $(FEATURE_EXTRACT_UTILS) --format "{p}" --prefix none | sed -E -n 's/^uu_([^ ]+).*/\1/p') + ifeq ($(SELINUX_ENABLED),1) PROGS += $(SELINUX_PROGS) endif @@ -114,7 +99,7 @@ endif # Programs with usable tests TESTS := \ - $(sort $(filter $(UTILS),$(PROGS) $(UNIX_PROGS) $(SELINUX_PROGS))) + $(sort $(filter $(UTILS),$(PROGS) $(SELINUX_PROGS))) TEST_NO_FAIL_FAST := TEST_SPEC_FEATURE := @@ -288,7 +273,7 @@ install: build install-manpages install-completions install-locales mkdir -p $(INSTALLDIR_BIN) ifneq (,$(and $(findstring stdbuf,$(UTILS)),$(findstring feat_external_libstdbuf,$(CARGOFLAGS)))) mkdir -p $(DESTDIR)$(LIBSTDBUF_DIR) -ifneq (,$(findstring CYGWIN,$(OS))) +ifneq (,$(findstring cygwin,$(OS))) $(INSTALL) -m 755 $(BUILDDIR)/deps/stdbuf.dll $(DESTDIR)$(LIBSTDBUF_DIR)/libstdbuf.dll else $(INSTALL) -m 755 $(BUILDDIR)/deps/libstdbuf.* $(DESTDIR)$(LIBSTDBUF_DIR)/ @@ -308,7 +293,7 @@ else endif uninstall: -ifeq (,$(findstring MINGW,$(OS))) +ifeq (,$(findstring windows,$(OS))) rm -f $(DESTDIR)$(LIBSTDBUF_DIR)/libstdbuf.* -rm -d $(DESTDIR)$(LIBSTDBUF_DIR) 2>/dev/null || true endif diff --git a/README.md b/README.md index 6708d9dcadc..007f19cdd89 100644 --- a/README.md +++ b/README.md @@ -111,8 +111,6 @@ sets of uutils for a platform (on that platform) is as simple as specifying it as a feature: ```shell -cargo build --release --features macos -# or ... cargo build --release --features windows # or ... cargo build --release --features unix diff --git a/deny.toml b/deny.toml index 2db6d69d05e..ea8396df467 100644 --- a/deny.toml +++ b/deny.toml @@ -108,6 +108,12 @@ skip = [ { name = "foldhash", version = "0.1.5" }, # keccak, sha1, sha2 { name = "cpufeatures", version = "0.2.17" }, + # various crates + { name = "digest", version = "0.10.7" }, + # digest + { name = "crypto-common", version = "0.1.7" }, + # digest + { name = "block-buffer", version = "0.10.4" }, ] # spell-checker: enable diff --git a/docs/src/installation.md b/docs/src/installation.md index 8ff0f004efd..a62b6fa8ab4 100644 --- a/docs/src/installation.md +++ b/docs/src/installation.md @@ -15,10 +15,8 @@ You can also [build uutils from source](build.md). [![crates.io package](https://repology.org/badge/version-for-repo/crates_io/uutils-coreutils.svg)](https://crates.io/crates/coreutils) ```shell -# Linux +# Unix like cargo install coreutils --features unix --locked -# MacOs -cargo install coreutils --features macos --locked # Windows cargo install coreutils --features windows --locked ``` diff --git a/docs/src/release-notes/0.7.0.md b/docs/src/release-notes/0.7.0.md new file mode 100644 index 00000000000..2a8a858340e --- /dev/null +++ b/docs/src/release-notes/0.7.0.md @@ -0,0 +1,518 @@ +### **Rust Coreutils 0.7.0 Release:** + +We are excited to announce the release of **Rust Coreutils 0.7.0** — a performance-focused release with **major optimizations across dozens of utilities**, continued safety improvements replacing unsafe code with safe abstractions, and a comprehensive campaign to eliminate panics on write errors. We also contributed many patches upstream to GNU coreutils, and welcomed their feedbacks and supports, strengthening both projects! + +--- + +### GNU Test Suite Compatibility: + +| Result | 0.6.0 | 0.7.0 | Change 0.6.0 to 0.7.0 | % Total 0.6.0 | % Total 0.7.0 | % Change 0.6.0 to 0.7.0 | +|---------------|-------|-------|------------------------|---------------|---------------|--------------------------| +| Pass | 622 | 629 | +7 | 96.28% | 94.59% | -1.69% | +| Skip | 7 | 13 | +6 | 1.08% | 1.95% | +0.87% | +| Fail | 16 | 23 | +7 | 2.48% | 3.46% | +0.98% | +| Error | 1 | 0 | -1 | 0.15% | 0% | -0.15% | +| Total | 646 | 665 | +19 (new tests) | | | | + +> **Note:** The GNU test reference was updated from 9.9 to 9.10, adding **19 new tests**. While the pass percentage decreased due to these newly added tests, the absolute number of passing tests increased by 7 and errors were eliminated entirely. Work is ongoing to address the new test failures. + +--- + +![GNU testsuite evolution](https://github.com/uutils/coreutils-tracking/blob/main/gnu-results.svg?raw=true) + +--- + +### Highlights: + +- **GNU Compatibility & Upstream Contributions** + - **629 passing tests** (+7 from 0.6.0), with **19 new tests** added from the GNU 9.10 update + - Updated GNU test reference from 9.9 to 9.10 + - Contributed numerous patches upstream to GNU coreutils, benefiting both projects + - New GNU compatibility fixes across `date`, `fmt`, `kill`, `ptx`, `numfmt`, `cksum`, and more + - Took over maintenance of [`num-prime`](https://github.com/uutils/num-prime), the primality testing library used by `factor` + +- **Performance Overhaul** + - Faster hash maps: `rustc-hash` in `ls`, `du`, `tsort`, `shuf`, `mv`; `foldhash` in `sort` + - `unexpand`/`expand`: ASCII fast-path, buffered reads — 14%+ gain in `unexpand` + - `shuf`, `split`, `sort`, `du`: Reduced malloc allocations (+3–6% in `du`, +4% in `shuf`) + - `nl`: Optimized with `itoa` and direct writing + - `true`/`false`: Removed `clap` dependency, smaller binary, faster startup + - `uucore`: Disabled signal setup in simple utilities for binary size and startup speed + +- **Robustness: Eliminated `/dev/full` Panics** + - Fixed panics when stderr is `/dev/full` across **20+ utilities** (`echo`, `date`, `sort`, `expr`, `hostname`, `id`, `comm`, `pr`, `dircolors`, and more) + - Generic fix ensuring unrecognized options with `2>/dev/full` do not abort + +- **Safety & Code Quality** + - Replaced unsafe `libc` calls with safe `nix` crate wrappers in `uucore` (`umask`, `mkdirat`, and more) and `mknod` + - Eliminated TOCTOU races in `ln`, `tac`, and `install -D` + - `rm`: `--preserve-root` now works correctly on symlinks + - MSRV updated to **1.88** + +- **Notable Bug Fixes** + - `date`: Extensive fixes — `-u`/`-s`/`-d` flags, timezone abbreviation lookup and DST, RFC-822 format, `%+`/`%_` modifiers, `--debug`, locale `date_fmt` + - `cp`: Readonly directories, `-a`/`-z` flags, special files, non-UTF-8 directory names + - `mv`: Preserve symlinks during cross-device moves, handle FIFOs in directories + - `ls`: Hyperlink OSC 8 format, dired reports, fd leak on deep recursion, invalid UTF-8 hidden files + - `sort`: Collator panic in worker threads, scientific notation parsing + - `paste`: Multi-byte delimiters, GNU escape sequences, bounded buffering + - `printf`: `%q` shell quoting with control chars and quotes + - `ptx`: `-t`/`--typeset-mode`, multibyte Unicode panic, GNU default behavior + - `numfmt`: `--debug` flag, empty delimiter, null byte handling, error message formatting + - `cksum`: SHAKE algorithms, `--binary`/`--text`/`--tag` errors + - `cut`, `tac`, `tail`, `tr`, `uniq`, `od`, `chroot`, `stat`, `mktemp`, `pr`, `readlink`, `ln`, `kill`, `nproc`, `rm`, `env`, `sync`, `fmt`, `factor`, `wc`: Various GNU compatibility and correctness fixes + +- **Platform Support** + - NetBSD and PowerPC build fixes + - Windows: `tac` stdin piping, `test -r/-w/-x`, publish static `*.exe` binaries + - WebAssembly: Publish `*.wasm` artifacts + - `stdbuf`: Support `libstdbuf` in same directory as binary + - NixOS test compatibility fix; added security audit workflow + +- **Contributions**: This release was made possible by **23 new contributors** joining our community + +--- + +### Call to Action: + +**Help us translate** - Contribute translations at [Weblate](https://hosted.weblate.org/projects/rust-coreutils/) +**Sponsor us on GitHub** to accelerate development: [github.com/sponsors/uutils](https://github.com/sponsors/uutils) + +## What's Changed + +## cat +* cat: strip errno by @oech3 in https://github.com/uutils/coreutils/pull/10885 + +## cksum +* *sum: Fix locales fetching from `checksum_common` after installation by @RenjiSann in https://github.com/uutils/coreutils/pull/10575 +* *sum: Fix read_byte_lines discarding read errors by @RenjiSann in https://github.com/uutils/coreutils/pull/10671 +* test/cksum: implement `test_signed_checksums` by @0xMillyByte in https://github.com/uutils/coreutils/pull/10714 +* cksum: Accept SHAKE algorithms by @RenjiSann in https://github.com/uutils/coreutils/pull/10772 +* cksum family: Backport new errors for --binary, --text and --tag by @oech3 in https://github.com/uutils/coreutils/pull/10618 +* cksum family: Fix clippy::unnecessary_wraps by @oech3 in https://github.com/uutils/coreutils/pull/11110 + +## chroot +* chroot: fix gid being set by uid with --userspec #10307 by @cerdelen in https://github.com/uutils/coreutils/pull/10465 +* chroot: use var_os by @xtqqczze in https://github.com/uutils/coreutils/pull/11070 + +## comm +* comm /etc/pacman.conf /dev/null 2>/dev/full does not abort by @oech3 in https://github.com/uutils/coreutils/pull/10746 +* date, comm, tty: Fixing handling output to dev/null by @ChrisDryden in https://github.com/uutils/coreutils/pull/10888 + +## coreutils +* tests/misc/coreutils.sh: Fail with invalid binary name by @oech3 in https://github.com/uutils/coreutils/pull/10258 +* coreutils: output expected error for unrecognized options by @ChrisDryden in https://github.com/uutils/coreutils/pull/9869 +* coreutils: Let the name *utils valid by @oech3 in https://github.com/uutils/coreutils/pull/10729 +* coreutils: Fix 2>/dev/full aborts & drop a sed for GnuTests by @oech3 in https://github.com/uutils/coreutils/pull/10740 +* all: --typo 2>/dev/full does not abort by @oech3 in https://github.com/uutils/coreutils/pull/10764 +* Add regression test for coreutils --list by @oech3 in https://github.com/uutils/coreutils/pull/10858 + +## cp +* cp: fix recursive copy of readonly directories by @nikolalukovic in https://github.com/uutils/coreutils/pull/10529 +* cp: improve code clarity and remove redundant filesystem checks by @sylvestre in https://github.com/uutils/coreutils/pull/10790 +* cp: fixing cp -a functionality to match gnu implementation for -z flag handling and for folders by @ChrisDryden in https://github.com/uutils/coreutils/pull/10207 +* cp: Fix panic when recursively copying a directory with a non-UTF8 name by @aweinstock314 in https://github.com/uutils/coreutils/pull/11148 +* cp: handle special files by @victor-prokhorov in https://github.com/uutils/coreutils/pull/11163 + +## csplit +* refactor(csplit): use &str slices for patterns by @xtqqczze in https://github.com/uutils/coreutils/pull/11013 +* csplit: add benchmarks for line number and regex pattern splitting by @sylvestre in https://github.com/uutils/coreutils/pull/10927 + +## cut +* cut: fix -s flag for newline delimiter and optimize memory allocation by @akervald in https://github.com/uutils/coreutils/pull/11143 +* cut: two simple refactorings by @cakebaker in https://github.com/uutils/coreutils/pull/11194 + +## date +* date: fix -u flag to match GNU behavior for input parsing by @ChrisDryden in https://github.com/uutils/coreutils/pull/10715 +* date: Fix format optional argument to capture all following parameters by @cerdelen in https://github.com/uutils/coreutils/pull/10914 +* date: fix -s UTC conversion losing timezone offset by @yachi in https://github.com/uutils/coreutils/pull/10828 +* date: fix RFC-822 format to always use English names by @sylvestre in https://github.com/uutils/coreutils/pull/10932 +* date: bump parse_datetime & add test for leap-1 GNU test by @sylvestre in https://github.com/uutils/coreutils/pull/10933 +* date: add tests to match GNU's by @sylvestre in https://github.com/uutils/coreutils/pull/10939 +* date: fix double periods in Hungarian month abbreviations by @naoNao89 in https://github.com/uutils/coreutils/pull/10945 +* date: fix subfmt-up1, fill-1, pct-pct, and invalid-high-bit-set tests / implement --debug by @sylvestre in https://github.com/uutils/coreutils/pull/10940 +* date: use locale date_fmt instead of D_T_FMT for default format by @ChrisDryden in https://github.com/uutils/coreutils/pull/10935 +* date: Fix Error message missing '+' for format string after valid d flag by @cerdelen in https://github.com/uutils/coreutils/pull/10982 +* date: fix -d with relative dates and timezone abbreviations by @yachi in https://github.com/uutils/coreutils/pull/10956 +* date: fix timezone abbreviations using wrong offset outside their DST season by @aguimaraes in https://github.com/uutils/coreutils/pull/11045 +* date: fix %+ and %_ modifier edge cases by @naoNao89 in https://github.com/uutils/coreutils/pull/10999 +* date: extend tz abbreviation lookup by @cerdelen in https://github.com/uutils/coreutils/pull/11229 +* date: Remove eprintln! to avoid 2>/dev/full abort by @oech3 in https://github.com/uutils/coreutils/pull/11228 +* date, comm, tty: Fixing handling output to dev/null by @ChrisDryden in https://github.com/uutils/coreutils/pull/10888 + +## dd +* dd: simplify signal handling by removing Alarm timer thread by @ChrisDryden in https://github.com/uutils/coreutils/pull/10768 + +## df +* df: fallback when proc masked by @ChrisDryden in https://github.com/uutils/coreutils/pull/10417 + +## dircolors +* dircolors >/dev/full panics by @oech3 in https://github.com/uutils/coreutils/pull/10948 + +## du +* du: deduplicate Stat::new call by @svlv in https://github.com/uutils/coreutils/pull/10584 +* du: Use rustc-hash for du -a / performance by @oech3 in https://github.com/uutils/coreutils/pull/10663 +* du: Flags 'm', 'k', 'm' should be POSIX style overriden by @cerdelen in https://github.com/uutils/coreutils/pull/10664 +* Du size_format flag override by @cerdelen in https://github.com/uutils/coreutils/pull/10743 +* du: malloc perf +3~6% by @oech3 in https://github.com/uutils/coreutils/pull/11034 + +## echo +* echo --version >/dev/full panics by @oech3 in https://github.com/uutils/coreutils/pull/10853 + +## env +* env: fix regression of `--ignore-signal=PIPE` by @Ecordonnier in https://github.com/uutils/coreutils/pull/9618 + +## expand +* expand: remove read_until by @cerdelen in https://github.com/uutils/coreutils/pull/10657 +* expand: remove empty after help by @cakebaker in https://github.com/uutils/coreutils/pull/10977 +* expand: Fix performance drop with cgu=1 by ascii fast-path by @oech3 in https://github.com/uutils/coreutils/pull/11104 + +## expr +* expr --version >/dev/full panics & --help > /dev/full should fail by @oech3 in https://github.com/uutils/coreutils/pull/10854 + +## factor +* factor: add a test for a num-prime issue by @sylvestre in https://github.com/uutils/coreutils/pull/11096 +* factor: trim also null-chars by @yotam-medini in https://github.com/uutils/coreutils/pull/11182 + +## false +* false: dedup set_exit_code(1) by @oech3 in https://github.com/uutils/coreutils/pull/10823 +* true, false: Fix broken pipe by @oech3 in https://github.com/uutils/coreutils/pull/11204 +* true, false: drop Vec for binary size and perf by @oech3 in https://github.com/uutils/coreutils/pull/11223 + +## fmt +* fmt: restore GNU compatibility for tests/fmt/width by @karanabe in https://github.com/uutils/coreutils/pull/11073 + +## fold +* fold: refactor compute_col_count and add character mode tests by @ChrisDryden in https://github.com/uutils/coreutils/pull/10533 +* fold: replace truncate(0) with clear() by @xtqqczze in https://github.com/uutils/coreutils/pull/11059 + +## hostname +* hostname: fix panic on hostname > /dev/full by @WhateverAWS in https://github.com/uutils/coreutils/pull/10912 +* feat(hostname): add benchmark with large /etc/hosts by @naoNao89 in https://github.com/uutils/coreutils/pull/10979 + +## id +* Id: don't panic on write error by @FidelSch in https://github.com/uutils/coreutils/pull/10769 + +## install +* Fixed permissions in install for unix by @max-amb in https://github.com/uutils/coreutils/pull/10564 +* fix(install): prevent symlink race condition in install -D (fixes #10013) by @abendrothj in https://github.com/uutils/coreutils/pull/10140 + +## kill +* kill: fix GNU compatibility tests for RTMIN and RTMAX by @karanabe in https://github.com/uutils/coreutils/pull/11224 + +## ln +* fix: eliminate TOCTOU races in ln and tac by deferring is_dir() checks by @abendrothj in https://github.com/uutils/coreutils/pull/10991 +* ln: Interactive and Force override each other instead of defaulting to Force if both are specified by @aweinstock314 in https://github.com/uutils/coreutils/pull/11129 + +## ls +* ls: Replace Fnv with rustc-hash by @oech3 in https://github.com/uutils/coreutils/pull/10686 +* ls: fix hyperlink functionality to use correct OSC 8 format and handle symlink targets by @sylvestre in https://github.com/uutils/coreutils/pull/10824 +* ls: fix ls dired reports by @mattsu2020 in https://github.com/uutils/coreutils/pull/10527 +* ls: release directory fds before recursing to avoid EMFILE by @ChrisDryden in https://github.com/uutils/coreutils/pull/10894 +* ls: Update French translations by @sylvestre in https://github.com/uutils/coreutils/pull/11003 +* ls: Use rustc-hash at colors by @oech3 in https://github.com/uutils/coreutils/pull/10700 +* ls: Treat paths starting with a dot as hidden even if they contain invalid UTF-8. by @aweinstock314 in https://github.com/uutils/coreutils/pull/11135 + +## mktemp +* mktemp: handle invalid UTF-8 in suffix gracefully by @sylvestre in https://github.com/uutils/coreutils/pull/10818 +* mktemp: use env::os_var by @xtqqczze in https://github.com/uutils/coreutils/pull/11113 +* mktemp: Don't panic when getrandom failed by @oech3 in https://github.com/uutils/coreutils/pull/11154 + +## mknod +* mKnod: Refactor to remove unsafe code by @mattsu2020 in https://github.com/uutils/coreutils/pull/10138 + +## mv +* mv: handle FIFOs inside directories during cross-partition move by @ChrisDryden in https://github.com/uutils/coreutils/pull/10857 +* fix(mv): correct selinux cfg gating for non-Linux platforms by @naoNao89 in https://github.com/uutils/coreutils/pull/10989 +* mv: Use rustc-hash by @oech3 in https://github.com/uutils/coreutils/pull/11010 +* mv: preserve symlinks during cross-device moves instead of expanding them by @sylvestre in https://github.com/uutils/coreutils/pull/10546 + +## nl +* perf(nl): optimize line numbering by using itoa and direct writing by @CrazyRoka in https://github.com/uutils/coreutils/pull/10757 + +## nproc +* nproc: process space in OMP_NUM_THREADS by @cuiweixie in https://github.com/uutils/coreutils/pull/10973 +* nproc: Cleanup a const by @oech3 in https://github.com/uutils/coreutils/pull/11191 +* nproc: Minor cleanup by @oech3 in https://github.com/uutils/coreutils/pull/11192 + +## numfmt +* numfmt: add --debug flag to print warnings about invalid input by @ChrisDryden in https://github.com/uutils/coreutils/pull/10110 +* numfmt: numfmt --debug 2>/dev/full does not abort by @tuananh in https://github.com/uutils/coreutils/pull/10668 +* numfmt: fix empty delimiter and whitespace handling by @ChrisDryden in https://github.com/uutils/coreutils/pull/10350 +* numfmt: optimize output handling by using stdout directly by @xtqqczze in https://github.com/uutils/coreutils/pull/11051 +* fix(numfmt): Read lines only up to null byte (as GNU does) by @FidelSch in https://github.com/uutils/coreutils/pull/11146 +* fix(numfmt): format output on error messages by @FidelSch in https://github.com/uutils/coreutils/pull/11179 +* numfmt: stop a clone by @oech3 in https://github.com/uutils/coreutils/pull/11221 + +## od +* od: fix od -v /dev/zero panic by @mattsu2020 in https://github.com/uutils/coreutils/pull/10576 + +## paste +* paste: support multi-byte delimiters and GNU escape sequences by @ChrisDryden in https://github.com/uutils/coreutils/pull/10840 +* paste: avoid unbounded buffering for single input by @mattsu2020 in https://github.com/uutils/coreutils/pull/11060 + +## pr +* pr: fix column behavior for short files by @jfinkels in https://github.com/uutils/coreutils/pull/10379 +* pr missing 2>/dev/full does not abort by @oech3 in https://github.com/uutils/coreutils/pull/10731 +* pr: implement the -e flag by @cerdelen in https://github.com/uutils/coreutils/pull/10167 + +## printf +* printf: fix %q shell quoting with control chars and quotes by @sylvestre in https://github.com/uutils/coreutils/pull/10816 + +## ptx +* ptx: implement -t/--typeset-mode to change default width to 100 by @ChrisDryden in https://github.com/uutils/coreutils/pull/10856 +* ptx: fix panic when truncation string/keyword contain multibyte Unicode by @Xylphy in https://github.com/uutils/coreutils/pull/10836 +* ptx: match GNU default behavior by skipping non-alphabetic index tokens by @Xylphy in https://github.com/uutils/coreutils/pull/10919 + +## readlink +* readlink: Set silent mode as default by @denendaden in https://github.com/uutils/coreutils/pull/10711 + +## rm +* rm --preserve-root should work on symlink too by @sylvestre in https://github.com/uutils/coreutils/pull/9706 +* rm: report permission denied for unreadable subdirectories by @o1x3 in https://github.com/uutils/coreutils/pull/10974 + +## shuf +* shuf: Use rustc-hash for performance by @oech3 in https://github.com/uutils/coreutils/pull/10648 +* shuf: Drop inline after switched to fxhash by @oech3 in https://github.com/uutils/coreutils/pull/10781 +* shuf: Reduce malloc by @oech3 in https://github.com/uutils/coreutils/pull/10998 +* shuf: delete useless cast by @oech3 in https://github.com/uutils/coreutils/pull/11106 +* shuf: try Vec first and fallback to HashMap if it cause OOM by @oech3 in https://github.com/uutils/coreutils/pull/11169 +* shuf: Reduce malloc, perf +4% by @oech3 in https://github.com/uutils/coreutils/pull/11219 + +## sort +* sort: remove redundant if by @oech3 in https://github.com/uutils/coreutils/pull/10716 +* Sort: Use ahash by @oech3 in https://github.com/uutils/coreutils/pull/10783 +* sort: making ClosedCompressedTmpFile::reopen() not panic. by @devnexen in https://github.com/uutils/coreutils/pull/10807 +* sort: fix panic when collator not available in worker threads by @sylvestre in https://github.com/uutils/coreutils/pull/10915 +* fix: scientific notation is incorrectly parsed in general numeric sort by @Kaua-Klassmann in https://github.com/uutils/coreutils/pull/10437 +* sort: use LazyLock by @xtqqczze in https://github.com/uutils/coreutils/pull/10969 +* Sort debug message by @hlsxx in https://github.com/uutils/coreutils/pull/10960 +* sort: Replace malloc and 0 fill with huge reserve & min 0 fill by @oech3 in https://github.com/uutils/coreutils/pull/10975 +* sort: remove reserve which is difficult to understand by @oech3 in https://github.com/uutils/coreutils/pull/11040 +* sort: update DEFAULT_BUF_SIZE to 8 KiB by @xtqqczze in https://github.com/uutils/coreutils/pull/11041 +* sort: failed to set message sort by @hlsxx in https://github.com/uutils/coreutils/pull/11137 +* sort --compress-program missing 2>/dev/full does not abort by @oech3 in https://github.com/uutils/coreutils/pull/10951 +* deps: replace ahash with foldhash by @xtqqczze in https://github.com/uutils/coreutils/pull/11187 + +## split +* perf(split): reuse buffer in chunked splitting loop by @CrazyRoka in https://github.com/uutils/coreutils/pull/10695 +* perf(split): optimize FixedWidthNumber Display implementation by @CrazyRoka in https://github.com/uutils/coreutils/pull/10723 +* split: Reduce malloc by @oech3 in https://github.com/uutils/coreutils/pull/10976 + +## stat +* stat: fix mount table read when /proc is unavailable by @sylvestre in https://github.com/uutils/coreutils/pull/10300 +* print_numeric: print mode in octal when the mode is too large by @Connor-GH in https://github.com/uutils/coreutils/pull/10208 + +## stdbuf +* stdbuf: support libstdbuf in same directory as stdbuf by @Ecordonnier in https://github.com/uutils/coreutils/pull/10352 +* stdbuf: fix warning from `uninlined_format_args` by @cakebaker in https://github.com/uutils/coreutils/pull/10784 + +## stty +* stty: cleanup cfg by @oech3 in https://github.com/uutils/coreutils/pull/10879 +* stty: fix compilation on PowerPC architectures by @sylvestre in https://github.com/uutils/coreutils/pull/11050 + +## sync +* sync: handle fcntl errors with localized warnings by @sylvestre in https://github.com/uutils/coreutils/pull/10330 +* sync: open file with nonblock by @reubenwong97 in https://github.com/uutils/coreutils/pull/10765 +* sync: return after checking all inputs by @reubenwong97 in https://github.com/uutils/coreutils/pull/10742 + +## tac +* tac: fix stdin piping on Windows by skipping empty mmap results by @ChrisDryden in https://github.com/uutils/coreutils/pull/10795 +* tac: support non-UTF-8 separator by @victor-prokhorov in https://github.com/uutils/coreutils/pull/10934 + +## tail +* tail: report PermissionDenied instead of No such file when metadata fails by @RedNhight in https://github.com/uutils/coreutils/pull/11018 +* tests/tail: reduce delays in multiple tests to speed up execution by @domysu in https://github.com/uutils/coreutils/pull/11206 + +## test +* test: make the man-page readable + sync by @matttbe in https://github.com/uutils/coreutils/pull/10737 +* test: implement -r/-w/-x on Windows by @anihal in https://github.com/uutils/coreutils/pull/11120 + +## tr +* tr: fix possible usage in invalid utf8 set sequence. by @devnexen in https://github.com/uutils/coreutils/pull/10791 +* tr: simplify truncate logic by @xtqqczze in https://github.com/uutils/coreutils/pull/11072 + +## true +* true/false: remove large clap call by @my4ng in https://github.com/uutils/coreutils/pull/10673 +* true, false: Improve perf & fix clippy::unnecessary_wraps by @oech3 in https://github.com/uutils/coreutils/pull/11200 +* feat(true/false): add benchmarks for startup performance by @naoNao89 in https://github.com/uutils/coreutils/pull/10996 + +## tsort +* tsort: Use rustc-hash by @oech3 in https://github.com/uutils/coreutils/pull/10680 + +## unexpand +* Unexpand use buffered reads + tests by @cerdelen in https://github.com/uutils/coreutils/pull/10831 +* unexpand: codegen-units=1 by @oech3 in https://github.com/uutils/coreutils/pull/10817 +* unexpand: Refactor functions to use less parameters by @cerdelen in https://github.com/uutils/coreutils/pull/10900 +* unexpand: ascii fast-path by @oech3 in https://github.com/uutils/coreutils/pull/11114 +* unexpand: reuse a smaller buf, perf 14%+ by @oech3 in https://github.com/uutils/coreutils/pull/11178 + +## uniq +* uniq: fix -w to count bytes in C locale by @aguimaraes in https://github.com/uutils/coreutils/pull/11061 + +## uptime +* uptime: fix "unused import" warnings in test file by @cakebaker in https://github.com/uutils/coreutils/pull/10651 +* uptime: Remove wincode and wincode-derive dependencies by @ChrisDryden in https://github.com/uutils/coreutils/pull/10802 +* fix: uptime > /dev/full panic fix by @hlsxx in https://github.com/uutils/coreutils/pull/10827 + +## vdir +* vdir: make vdir an alias of ls to resolve missing ftl by @NatsuCamellia in https://github.com/uutils/coreutils/pull/10434 + +## wc +* wc: stop processing --files0-from input after stdout write failure by @mattsu2020 in https://github.com/uutils/coreutils/pull/11023 + +## whoami +* whoami: fix usage line for unexpected arguments by @YuF-9468 in https://github.com/uutils/coreutils/pull/11159 + +## uucore +* uucore: refactor digest_reader to use ReadingMode enum by @0xMillyByte in https://github.com/uutils/coreutils/pull/10720 +* uucore: Use `nix::sys::stat::umask` in `uucore::mode::get_umask` by @mattsu2020 in https://github.com/uutils/coreutils/pull/11102 +* uucore: fix correct mkdirat implementation to use nix crate's mkdirat function by @mattsu2020 in https://github.com/uutils/coreutils/pull/11126 +* uucore: replace unsafe libc calls with safe nix crate wrappers by @mattsu2020 in https://github.com/uutils/coreutils/pull/11156 +* uucore: use transmute instead of raw pointers by @xtqqczze in https://github.com/uutils/coreutils/pull/11077 +* uucore: Disallow slashes in determine_backup_suffix by @aweinstock314 in https://github.com/uutils/coreutils/pull/11149 +* uucore: disable signals at simple utils for binary size and fast startup by @oech3 in https://github.com/uutils/coreutils/pull/11186 +* dedup high-cost localization setup by @oech3 in https://github.com/uutils/coreutils/pull/11147 + +## CI & Build +* Catch regressions at early stage by @oech3 in https://github.com/uutils/coreutils/pull/10627 +* ci: enable memory profiling again by @not-matthias in https://github.com/uutils/coreutils/pull/10659 +* Revert "CICD.yml: upload binaries without version string" by @sylvestre in https://github.com/uutils/coreutils/pull/10676 +* Fix tag/latest-commit & comment naming policy by @oech3 in https://github.com/uutils/coreutils/pull/10684 +* fuzzing.yml: Use prebuilt for faster setup by @oech3 in https://github.com/uutils/coreutils/pull/10498 +* chore: run pre-commit on all files by @aaron-ang in https://github.com/uutils/coreutils/pull/10119 +* MSRV 1.88 by @oech3 in https://github.com/uutils/coreutils/pull/10771 +* Drop release-fast profile for simplicity by @oech3 in https://github.com/uutils/coreutils/pull/10476 +* actions: add security audit workflow by @xtqqczze in https://github.com/uutils/coreutils/pull/10767 +* Publish *.wasm by @oech3 in https://github.com/uutils/coreutils/pull/10656 +* Add rust-version field to all Cargo.toml files by @xtqqczze in https://github.com/uutils/coreutils/pull/10778 +* GnuTests: 9.10, remove a bunch of backported tests by @oech3 in https://github.com/uutils/coreutils/pull/10721 +* build: sort coreutils entries at build time by @Xylphy in https://github.com/uutils/coreutils/pull/10820 +* GNUmakefile: Allow cross-build for Windows by @oech3 in https://github.com/uutils/coreutils/pull/10897 +* CICD: Publish static individual *.exe by @oech3 in https://github.com/uutils/coreutils/pull/10903 +* benchmarks.yml: Use prebuilt tools for reproducible benches by @oech3 in https://github.com/uutils/coreutils/pull/10497 +* ci: remove GNU build caching from GnuTests workflow by @ChrisDryden in https://github.com/uutils/coreutils/pull/10961 +* CICD: Introduce check-only for Redox CI by @oech3 in https://github.com/uutils/coreutils/pull/10950 +* rust: use const_locks feature by @xtqqczze in https://github.com/uutils/coreutils/pull/10968 +* uutests: preserve PATH in UCommand to fix NixOS test failures by @0xMillyByte in https://github.com/uutils/coreutils/pull/10959 +* build-gnu: fix factor tests being re-added by automake during make check by @ChrisDryden in https://github.com/uutils/coreutils/pull/10907 +* Prevent make check from rebuilding GNU binaries over uutils ones by @ChrisDryden in https://github.com/uutils/coreutils/pull/11049 +* Disable strip related tests on Android by @oech3 in https://github.com/uutils/coreutils/pull/11047 +* test_env: Make argv0 tests compatible with Ubuntu patch by @oech3 in https://github.com/uutils/coreutils/pull/10834 +* GnuTests: Use our libstdbuf.so by @oech3 in https://github.com/uutils/coreutils/pull/10793 +* Avoid non reproducible cache generation at codcov by @oech3 in https://github.com/uutils/coreutils/pull/10926 +* Use MULTICALL=y at toybox test for faster CI by @oech3 in https://github.com/uutils/coreutils/pull/10370 +* CICD: Drop a duplicated test producing huge caches by @oech3 in https://github.com/uutils/coreutils/pull/10513 +* Don't fail when sccache caused network error & mark 2 tests flakey by @oech3 in https://github.com/uutils/coreutils/pull/11098 +* Don't wrap rustc when sccache action caused network err by @oech3 in https://github.com/uutils/coreutils/pull/11141 +* Cargo.toml: Avoid huge diff generation at version bump by @oech3 in https://github.com/uutils/coreutils/pull/11131 +* Cargo.toml: Define feat_wasm by @oech3 in https://github.com/uutils/coreutils/pull/11074 +* Mark pr/bounded-memory flakey & drop 2 useless setup by @oech3 in https://github.com/uutils/coreutils/pull/10887 +* Mark date-locale-hour flakey & drop a useless cache action by @oech3 in https://github.com/uutils/coreutils/pull/11093 +* Updating intermittent gnu test failures to reflect recent changes by @ChrisDryden in https://github.com/uutils/coreutils/pull/10804 + +## Documentation +* README.md: Don't recommend latest-commit for everyone by @oech3 in https://github.com/uutils/coreutils/pull/10678 +* add 0.5.0 & 0.6.0 release notes by @sylvestre in https://github.com/uutils/coreutils/pull/10709 +* Remove the word: hashsums by @oech3 in https://github.com/uutils/coreutils/pull/10674 +* fix docs hashsumS and test by @Its-Just-Nans in https://github.com/uutils/coreutils/pull/7936 +* head: improve help grammar by @memark in https://github.com/uutils/coreutils/pull/10848 +* Fix for potential typos by @lanceXwq in https://github.com/uutils/coreutils/pull/10822 +* chore: use consistent copyright header in `.rs` files by @xtqqczze in https://github.com/uutils/coreutils/pull/11020 + +## Code Quality & Cleanup +* clippy: fix nightly map_unwrap_or lint by @xtqqczze in https://github.com/uutils/coreutils/pull/10625 +* Re-enable unused_qualifications lint by @Xylphy in https://github.com/uutils/coreutils/pull/10571 +* chore: a few more Clippy fixes by @nyurik in https://github.com/uutils/coreutils/pull/10697 +* chore: minor optimizations by @nyurik in https://github.com/uutils/coreutils/pull/10707 +* chore: fix `clippy::single_match_else` by @nyurik in https://github.com/uutils/coreutils/pull/10699 +* chore: `clippy::unreadable_literal` by @nyurik in https://github.com/uutils/coreutils/pull/10717 +* refactor: removing unnecessary references in fn signatures by @nyurik in https://github.com/uutils/coreutils/pull/10703 +* chore: `clippy::wildcard_imports` by @nyurik in https://github.com/uutils/coreutils/pull/10719 +* chore: `clippy::unnecessary_wraps` by @nyurik in https://github.com/uutils/coreutils/pull/10722 +* chore: a few minor lints by @nyurik in https://github.com/uutils/coreutils/pull/10754 +* chore: `clippy::redundant_closure_for_method_calls` by @nyurik in https://github.com/uutils/coreutils/pull/10704 +* refactor: inline format! args in a few places by @nyurik in https://github.com/uutils/coreutils/pull/10730 +* clippy: fix manual_is_multiple_of lint by @xtqqczze in https://github.com/uutils/coreutils/pull/10775 +* clippy: fix struct_field_names lint by @xtqqczze in https://github.com/uutils/coreutils/pull/10902 +* clippy: fix nightly lints by @xtqqczze in https://github.com/uutils/coreutils/pull/10970 +* clippy: fix lints by @xtqqczze in https://github.com/uutils/coreutils/pull/11000 +* clippy: fix nightly lints by @xtqqczze in https://github.com/uutils/coreutils/pull/11232 +* clippy: enable `struct_field_names` lint by @cakebaker in https://github.com/uutils/coreutils/pull/11214 +* chore: Enable workspace lints in uutest crate by @Xylphy in https://github.com/uutils/coreutils/pull/10727 +* deps: remove unused rust deps by @xtqqczze in https://github.com/uutils/coreutils/pull/10688 +* use zip for cleaner iteration with 1-based index by @xtqqczze in https://github.com/uutils/coreutils/pull/10963 +* refactor: simplify UError construction by @xtqqczze in https://github.com/uutils/coreutils/pull/11171 +* refactor: use slice::get for clarity by @xtqqczze in https://github.com/uutils/coreutils/pull/11108 +* tail: disallow clippy::wrong_self_convention by @oech3 in https://github.com/uutils/coreutils/pull/11145 +* Remove hashsum test files by @oech3 in https://github.com/uutils/coreutils/pull/11086 + +## Dependency Updates +* Bump wincode crates to `0.3.1` by @cakebaker in https://github.com/uutils/coreutils/pull/10650 +* chore(deps): update rust crate regex to v1.12.3 by @renovate[bot] in https://github.com/uutils/coreutils/pull/10683 +* chore(deps): update rust crate clap to v4.5.57 by @renovate[bot] in https://github.com/uutils/coreutils/pull/10685 +* chore(deps): update rust crate zip to v7.3.0 by @renovate[bot] in https://github.com/uutils/coreutils/pull/10713 +* chore(deps): update vmactions/freebsd-vm action to v1.3.9 by @renovate[bot] in https://github.com/uutils/coreutils/pull/9605 +* chore(deps): update rust crate jiff to v0.2.19 by @renovate[bot] in https://github.com/uutils/coreutils/pull/10760 +* chore(deps): update rust crate memchr to v2.8.0 by @renovate[bot] in https://github.com/uutils/coreutils/pull/10774 +* chore(deps): update rust crate libc to v0.2.181 by @renovate[bot] in https://github.com/uutils/coreutils/pull/10844 +* Revert "chore(deps): update rust crate libc to v0.2.181" by @xtqqczze in https://github.com/uutils/coreutils/pull/10875 +* chore(deps): update rust crate clap to v4.5.58 by @renovate[bot] in https://github.com/uutils/coreutils/pull/10876 +* chore(deps): update rust crate clap_complete to v4.5.66 by @renovate[bot] in https://github.com/uutils/coreutils/pull/10877 +* Bump `jiff` & fix clippy warning by @cakebaker in https://github.com/uutils/coreutils/pull/10881 +* chore(deps): update rust crate tempfile to v3.25.0 by @renovate[bot] in https://github.com/uutils/coreutils/pull/10843 +* chore(deps): update rust crate libfuzzer-sys to v0.4.12 by @renovate[bot] in https://github.com/uutils/coreutils/pull/10864 +* chore(deps): update rust crate z85 to v3.0.7 by @renovate[bot] in https://github.com/uutils/coreutils/pull/10913 +* chore(deps): update rust crate zip to v8 by @renovate[bot] in https://github.com/uutils/coreutils/pull/10944 +* chore(deps): update rust crate indicatif to v0.18.4 by @renovate[bot] in https://github.com/uutils/coreutils/pull/10936 +* chore(deps): update rust crate memmap2 to v0.9.10 by @renovate[bot] in https://github.com/uutils/coreutils/pull/10953 +* chore(deps): update rust crate clap to v4.5.59 by @renovate[bot] in https://github.com/uutils/coreutils/pull/10985 +* chore(deps): update rust crate zip to v8.1.0 by @renovate[bot] in https://github.com/uutils/coreutils/pull/10990 +* fix(deps): update rust crate keccak to v0.1.6 by @xtqqczze in https://github.com/uutils/coreutils/pull/10988 +* chore(deps): update dawidd6/action-download-artifact action to v15 by @renovate[bot] in https://github.com/uutils/coreutils/pull/11012 +* chore(deps): update rust crate clap to v4.5.60 by @renovate[bot] in https://github.com/uutils/coreutils/pull/11030 +* Bump fuzzer's libc to 0.2.182 by @oech3 in https://github.com/uutils/coreutils/pull/11037 +* fuzz: bump `keccak` from `0.1.5` to `0.1.6` by @cakebaker in https://github.com/uutils/coreutils/pull/11035 +* chore(deps): update rust crate rustix to 1.1.4 by @xtqqczze in https://github.com/uutils/coreutils/pull/11055 +* chore(deps): update rust crate jiff to v0.2.21 by @renovate[bot] in https://github.com/uutils/coreutils/pull/11058 +* chore(deps): update rust crate tempfile to v3.26.0 by @renovate[bot] in https://github.com/uutils/coreutils/pull/11087 +* Bump `num-prime` from `0.4.4` to `0.5.0` by @cakebaker in https://github.com/uutils/coreutils/pull/11069 +* deps: update rust crate futures-util to v0.3.32 by @xtqqczze in https://github.com/uutils/coreutils/pull/11094 +* chore(deps): update dawidd6/action-download-artifact action to v16 by @renovate[bot] in https://github.com/uutils/coreutils/pull/11121 +* chore(deps): update rust crate jiff to v0.2.22 by @renovate[bot] in https://github.com/uutils/coreutils/pull/11164 +* chore(deps): update rust crate zip to v8.2.0 by @renovate[bot] in https://github.com/uutils/coreutils/pull/11184 +* chore(deps): update rust crate quote to v1.0.45 by @renovate[bot] in https://github.com/uutils/coreutils/pull/11188 +* chore(deps): update rust crate jiff to v0.2.23 by @renovate[bot] in https://github.com/uutils/coreutils/pull/11189 +* chore(deps): update github artifact actions (major) by @renovate[bot] in https://github.com/uutils/coreutils/pull/11132 +* deps: replace ahash with foldhash by @xtqqczze in https://github.com/uutils/coreutils/pull/11187 +* deny.toml: remove `signal-hook` from skip list by @cakebaker in https://github.com/uutils/coreutils/pull/10785 + +## Version Management +* prepare release 0.7.0 by @sylvestre in https://github.com/uutils/coreutils/pull/11128 + +## New Contributors +* @my4ng made their first contribution in https://github.com/uutils/coreutils/pull/10673 +* @tuananh made their first contribution in https://github.com/uutils/coreutils/pull/10668 +* @0xMillyByte made their first contribution in https://github.com/uutils/coreutils/pull/10714 +* @NatsuCamellia made their first contribution in https://github.com/uutils/coreutils/pull/10434 +* @nikolalukovic made their first contribution in https://github.com/uutils/coreutils/pull/10529 +* @memark made their first contribution in https://github.com/uutils/coreutils/pull/10848 +* @lanceXwq made their first contribution in https://github.com/uutils/coreutils/pull/10822 +* @denendaden made their first contribution in https://github.com/uutils/coreutils/pull/10711 +* @hlsxx made their first contribution in https://github.com/uutils/coreutils/pull/10827 +* @WhateverAWS made their first contribution in https://github.com/uutils/coreutils/pull/10912 +* @vjardin made their first contribution in https://github.com/uutils/coreutils/pull/10901 +* @yachi made their first contribution in https://github.com/uutils/coreutils/pull/10828 +* @cuiweixie made their first contribution in https://github.com/uutils/coreutils/pull/10973 +* @o1x3 made their first contribution in https://github.com/uutils/coreutils/pull/10974 +* @aguimaraes made their first contribution in https://github.com/uutils/coreutils/pull/11045 +* @victor-prokhorov made their first contribution in https://github.com/uutils/coreutils/pull/10934 +* @RedNhight made their first contribution in https://github.com/uutils/coreutils/pull/11018 +* @anihal made their first contribution in https://github.com/uutils/coreutils/pull/11112 +* @aweinstock314 made their first contribution in https://github.com/uutils/coreutils/pull/11129 +* @YuF-9468 made their first contribution in https://github.com/uutils/coreutils/pull/11159 +* @yotam-medini made their first contribution in https://github.com/uutils/coreutils/pull/11182 +* @akervald made their first contribution in https://github.com/uutils/coreutils/pull/11143 +* @domysu made their first contribution in https://github.com/uutils/coreutils/pull/11206 + +**Full Changelog**: https://github.com/uutils/coreutils/compare/0.6.0...0.7.0 diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index 35e7ed653b9..c4948a40430 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -62,7 +62,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -73,7 +73,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -178,6 +178,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +dependencies = [ + "hybrid-array", +] + [[package]] name = "block2" version = "0.6.2" @@ -318,6 +327,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "const-random" version = "0.1.18" @@ -383,7 +398,7 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e75b2483e97a5a7da73ac68a05b629f9c53cff58d8ed1c77866079e18b00dba5" dependencies = [ - "digest", + "digest 0.10.7", "spin", ] @@ -437,6 +452,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" +dependencies = [ + "hybrid-array", +] + [[package]] name = "ctrlc" version = "3.5.2" @@ -480,8 +504,19 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "crypto-common", + "block-buffer 0.10.4", + "crypto-common 0.1.7", +] + +[[package]] +name = "digest" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" +dependencies = [ + "block-buffer 0.12.0", + "const-oid", + "crypto-common 0.2.1", ] [[package]] @@ -547,7 +582,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -726,6 +761,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hybrid-array" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8655f91cd07f2b9d0c24137bd650fe69617773435ee5ec83022377777ce65ef1" +dependencies = [ + "typenum", +] + [[package]] name = "iana-time-zone" version = "0.1.65" @@ -1081,7 +1125,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde_core", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1203,7 +1247,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ "cfg-if", - "digest", + "digest 0.10.7", ] [[package]] @@ -1576,7 +1620,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1648,7 +1692,7 @@ checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures 0.2.17", - "digest", + "digest 0.10.7", ] [[package]] @@ -1659,7 +1703,7 @@ checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures 0.2.17", - "digest", + "digest 0.10.7", ] [[package]] @@ -1668,7 +1712,7 @@ version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" dependencies = [ - "digest", + "digest 0.10.7", "keccak", ] @@ -1686,17 +1730,20 @@ checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" [[package]] name = "similar" -version = "2.7.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" +checksum = "26d0b06eba54f0ca0770f970a3e89823e766ca638dd940f8469fa0fa50c75396" +dependencies = [ + "bstr", +] [[package]] name = "sm3" -version = "0.4.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebb9a3b702d0a7e33bc4d85a14456633d2b165c2ad839c5fd9a8417c1ab15860" +checksum = "da6a89ba31723d185fd7413b98c576a575f356d9b84729d8ecb6ead60000a5b6" dependencies = [ - "digest", + "digest 0.11.2", ] [[package]] @@ -1755,7 +1802,7 @@ dependencies = [ "getrandom 0.4.1", "once_cell", "rustix", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1885,7 +1932,7 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uu_checksum_common" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -1894,7 +1941,7 @@ dependencies = [ [[package]] name = "uu_cksum" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -1904,7 +1951,7 @@ dependencies = [ [[package]] name = "uu_cut" -version = "0.7.0" +version = "0.8.0" dependencies = [ "bstr", "clap", @@ -1915,7 +1962,7 @@ dependencies = [ [[package]] name = "uu_date" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -1923,16 +1970,17 @@ dependencies = [ "icu_locale", "jiff", "jiff-icu", - "nix", + "libc", "parse_datetime", "regex", + "rustix", "uucore", "windows-sys 0.61.2", ] [[package]] name = "uu_dirname" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -1941,7 +1989,7 @@ dependencies = [ [[package]] name = "uu_echo" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -1950,7 +1998,7 @@ dependencies = [ [[package]] name = "uu_env" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -1962,7 +2010,7 @@ dependencies = [ [[package]] name = "uu_expr" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -1975,7 +2023,7 @@ dependencies = [ [[package]] name = "uu_printf" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -1984,7 +2032,7 @@ dependencies = [ [[package]] name = "uu_seq" -version = "0.7.0" +version = "0.8.0" dependencies = [ "bigdecimal", "clap", @@ -1997,7 +2045,7 @@ dependencies = [ [[package]] name = "uu_sort" -version = "0.7.0" +version = "0.8.0" dependencies = [ "bigdecimal", "binary-heap-plus", @@ -2019,7 +2067,7 @@ dependencies = [ [[package]] name = "uu_split" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -2030,7 +2078,7 @@ dependencies = [ [[package]] name = "uu_test" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "fluent", @@ -2041,7 +2089,7 @@ dependencies = [ [[package]] name = "uu_tr" -version = "0.7.0" +version = "0.8.0" dependencies = [ "bytecount", "clap", @@ -2052,13 +2100,13 @@ dependencies = [ [[package]] name = "uu_wc" -version = "0.7.0" +version = "0.8.0" dependencies = [ "bytecount", "clap", "fluent", "libc", - "nix", + "rustix", "thiserror", "unicode-width", "uucore", @@ -2066,7 +2114,7 @@ dependencies = [ [[package]] name = "uucore" -version = "0.7.0" +version = "0.8.0" dependencies = [ "base64-simd", "bigdecimal", @@ -2076,7 +2124,7 @@ dependencies = [ "crc-fast", "data-encoding", "data-encoding-macro", - "digest", + "digest 0.10.7", "dunce", "fluent", "fluent-bundle", @@ -2100,6 +2148,7 @@ dependencies = [ "os_display", "procfs", "rustc-hash", + "rustix", "sha1", "sha2", "sha3", @@ -2140,7 +2189,7 @@ dependencies = [ [[package]] name = "uucore_procs" -version = "0.7.0" +version = "0.8.0" dependencies = [ "proc-macro2", "quote", @@ -2148,7 +2197,7 @@ dependencies = [ [[package]] name = "uufuzz" -version = "0.7.0" +version = "0.8.0" dependencies = [ "console", "libc", @@ -2288,7 +2337,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] diff --git a/fuzz/uufuzz/Cargo.toml b/fuzz/uufuzz/Cargo.toml index 4480061763c..2ca4ff9f3dd 100644 --- a/fuzz/uufuzz/Cargo.toml +++ b/fuzz/uufuzz/Cargo.toml @@ -3,7 +3,7 @@ name = "uufuzz" authors = ["uutils developers"] description = "uutils ~ 'core' uutils fuzzing library" repository = "https://github.com/uutils/coreutils/tree/main/fuzz/uufuzz" -version = "0.7.0" +version = "0.8.0" edition.workspace = true rust-version.workspace = true license.workspace = true @@ -12,6 +12,6 @@ license.workspace = true console = "0.16.0" libc = "0.2.153" rand = { version = "0.9.0", features = ["small_rng"] } -similar = "2.5.0" -uucore = { version = "0.7.0", path = "../../src/uucore", features = ["parser"] } +similar = "3.0.0" +uucore = { version = "0.8.0", path = "../../src/uucore", features = ["parser"] } tempfile = "3.15.0" diff --git a/src/bin/coreutils.rs b/src/bin/coreutils.rs index 59634849a6b..84c7ff0ab85 100644 --- a/src/bin/coreutils.rs +++ b/src/bin/coreutils.rs @@ -10,38 +10,47 @@ use std::cmp; use std::ffi::OsString; use std::io::{self, Write}; use std::process; -use uucore::Args; +use uucore::{Args, error::strip_errno}; const VERSION: &str = env!("CARGO_PKG_VERSION"); include!(concat!(env!("OUT_DIR"), "/uutils_map.rs")); fn usage(utils: &UtilityMap, name: &str) { - println!("{name} {VERSION} (multi-call binary)\n"); - println!("Usage: {name} [function [arguments...]]"); - println!(" {name} --list"); - println!(); + let display_list = utils.keys().copied().join(", "); + let width = cmp::min(textwrap::termwidth(), 100) - 8; // (opinion/heuristic) max 100 chars wide with 4 character side indentions + let indent_list = textwrap::indent(&textwrap::fill(&display_list, width), " "); #[cfg(feature = "feat_common_core")] + let common_core_string = " +Functions: + '' [arguments...] + +"; + #[cfg(not(feature = "feat_common_core"))] + let common_core_string = ""; + let s = format!( + "{name} {VERSION} (multi-call binary) + +Usage: {name} [function [arguments...]] + {name} --list + +{common_core_string}Options: + --list lists all defined functions, one per row + +Currently defined functions: + +{indent_list}" + ); + if let Err(e) = writeln!(io::stdout(), "{s}") + && e.kind() != io::ErrorKind::BrokenPipe { - println!("Functions:"); - println!(" '' [arguments...]"); - println!(); + let _ = writeln!(io::stderr(), "coreutils: {}", strip_errno(&e)); + process::exit(1); } - println!("Options:"); - println!(" --list lists all defined functions, one per row\n"); - println!("Currently defined functions:\n"); - let display_list = utils.keys().copied().join(", "); - let width = cmp::min(textwrap::termwidth(), 100) - 4 * 2; // (opinion/heuristic) max 100 chars wide with 4 character side indentions - println!( - "{}", - textwrap::indent(&textwrap::fill(&display_list, width), " ") - ); } #[allow(clippy::cognitive_complexity)] fn main() { - uucore::panic::mute_sigpipe_panic(); - let utils = util_map(); let mut args = uucore::args_os(); @@ -76,19 +85,29 @@ fn main() { match util { "--list" => { - // If --help is also present, show usage instead of list - if args.any(|arg| arg == "--help" || arg == "-h") { - usage(&utils, binary_as_util); - process::exit(0); + // we should fail with additional args https://github.com/uutils/coreutils/issues/11383#issuecomment-4082564058 + if args.next().is_some() { + let _ = writeln!(io::stderr(), "coreutils: invalid argument"); + process::exit(1); } - let utils: Vec<_> = utils.keys().collect(); - for util in utils { - println!("{util}"); + let mut out = io::stdout().lock(); + for util in utils.keys() { + if let Err(e) = writeln!(out, "{util}") + && e.kind() != io::ErrorKind::BrokenPipe + { + let _ = writeln!(io::stderr(), "coreutils: {}", strip_errno(&e)); + process::exit(1); + } } process::exit(0); } "--version" | "-V" => { - println!("{binary_as_util} {VERSION} (multi-call binary)"); + if let Err(e) = writeln!(io::stdout(), "coreutils {VERSION} (multi-call binary)") + && e.kind() != io::ErrorKind::BrokenPipe + { + let _ = writeln!(io::stderr(), "coreutils: {}", strip_errno(&e)); + process::exit(1); + } process::exit(0); } // Not a special command: fallthrough to calling a util @@ -137,8 +156,13 @@ fn main() { } } } else { - // no arguments provided - usage(&utils, binary_as_util); - process::exit(0); + // GNU just fails, but busybox tests needs usage + // todo: patch the test suite instead + if binary_as_util.ends_with("box") { + usage(&utils, binary_as_util); + } else { + let _ = writeln!(io::stderr(), "coreutils: missing argument"); + } + process::exit(1); } } diff --git a/src/bin/uudoc.rs b/src/bin/uudoc.rs index a2395d3aee7..1ef6c419395 100644 --- a/src/bin/uudoc.rs +++ b/src/bin/uudoc.rs @@ -25,6 +25,7 @@ use zip::ZipArchive; use coreutils::validation; use uucore::Args; +use uucore::locale::get_message; include!(concat!(env!("OUT_DIR"), "/uutils_map.rs")); @@ -246,6 +247,8 @@ fn main() -> io::Result<()> { } } let utils = util_map::>>(); + // Initialize localization for uucore common strings (used by tldr example attribution) + let _ = uucore::locale::setup_localization("uudoc"); match std::fs::create_dir("docs/src/utils/") { Err(e) if e.kind() == io::ErrorKind::AlreadyExists => Ok(()), x => x, @@ -492,7 +495,7 @@ impl MDWriter<'_, '_> { .iter() .any(|u| u == self.name) { - writeln!(self.w, "")?; + writeln!(self.w, "")?; } } writeln!(self.w, "")?; @@ -693,15 +696,9 @@ fn format_examples(content: String, output_markdown: bool) -> Result The examples are provided by the [tldr-pages project](https://tldr.sh) under the [CC BY 4.0 License](https://github.com/tldr-pages/tldr/blob/main/LICENSE.md)." - )?; + writeln!(s, "> {}", get_message("uudoc-tldr-attribution"))?; writeln!(s, ">")?; - writeln!( - s, - "> Please note that, as uutils is a work in progress, some examples might fail." - )?; + writeln!(s, "> {}", get_message("uudoc-tldr-disclaimer"))?; Ok(s) } diff --git a/src/common/validation.rs b/src/common/validation.rs index a0a13b5df9a..ad55165df43 100644 --- a/src/common/validation.rs +++ b/src/common/validation.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 prefixcat testcat +// spell-checker:ignore memfd_create prefixcat rsplit testcat use std::ffi::{OsStr, OsString}; use std::io::{Write, stderr}; @@ -73,15 +73,41 @@ fn get_canonical_util_name(util_name: &str) -> &str { } /// Gets the binary path from command line arguments -/// # Panics /// Panics if the binary path cannot be determined +#[cfg(not(any(target_os = "linux", target_os = "android")))] pub fn binary_path(args: &mut impl Iterator) -> PathBuf { match args.next() { Some(ref s) if !s.is_empty() => PathBuf::from(s), + // the fallback is valid only for hardlinks _ => std::env::current_exe().unwrap(), } } - +/// Get actual binary path from kernel, not argv0, to prevent `env -a` from bypassing +/// AppArmor, SELinux policies on hard-linked binaries +#[cfg(any(target_os = "linux", target_os = "android"))] +pub fn binary_path(args: &mut impl Iterator) -> PathBuf { + use std::fs::File; + use std::io::Read; + use std::os::unix::ffi::OsStrExt; + let execfn = rustix::param::linux_execfn(); + let execfn_bytes = execfn.to_bytes(); + let exec_path = Path::new(OsStr::from_bytes(execfn_bytes)); + let argv0 = args.next().unwrap(); + let mut shebang_buf = [0u8; 2]; + // exec_path is wrong when called from shebang or memfd_create (/proc/self/fd/*) + // argv0 is not full-path when called from PATH + if execfn_bytes.rsplit(|&b| b == b'/').next() == argv0.as_bytes().rsplit(|&b| b == b'/').next() + || execfn_bytes.starts_with(b"/proc/") + || (File::open(Path::new(exec_path)) + .and_then(|mut f| f.read_exact(&mut shebang_buf)) + .is_ok() + && &shebang_buf == b"#!") + { + argv0.into() + } else { + exec_path.into() + } +} /// Extracts the binary name from a path pub fn name(binary_path: &Path) -> Option<&str> { binary_path.file_stem()?.to_str() diff --git a/src/uu/arch/src/arch.rs b/src/uu/arch/src/arch.rs index f07eb227c41..d0a197ece22 100644 --- a/src/uu/arch/src/arch.rs +++ b/src/uu/arch/src/arch.rs @@ -21,9 +21,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("arch") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("arch")) .about(translate!("arch-about")) .after_help(translate!("arch-after-help")) .override_usage(translate!("arch-usage")) diff --git a/src/uu/b2sum/src/b2sum.rs b/src/uu/b2sum/src/b2sum.rs index 6df276f2393..66227664192 100644 --- a/src/uu/b2sum/src/b2sum.rs +++ b/src/uu/b2sum/src/b2sum.rs @@ -9,18 +9,15 @@ use clap::Command; use uu_checksum_common::{standalone_checksum_app_with_length, standalone_with_length_main}; -use uucore::checksum::{AlgoKind, calculate_blake2b_length_str}; +use uucore::checksum::{AlgoKind, BlakeLength, parse_blake_length}; use uucore::error::UResult; use uucore::translate; #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - standalone_with_length_main( - AlgoKind::Blake2b, - uu_app(), - args, - calculate_blake2b_length_str, - ) + let calculate_blake2b_length = + |s: &str| parse_blake_length(AlgoKind::Blake2b, BlakeLength::String(s)); + standalone_with_length_main(AlgoKind::Blake2b, uu_app(), args, calculate_blake2b_length) } #[inline] diff --git a/src/uu/base32/src/base_common.rs b/src/uu/base32/src/base_common.rs index 0c7e13c23c0..4e82986737f 100644 --- a/src/uu/base32/src/base_common.rs +++ b/src/uu/base32/src/base_common.rs @@ -455,39 +455,37 @@ pub mod fast_encode { .iter() .enumerate() .step_by(encode_in_chunks_of_size) - .map(|(idx, _)| { + .filter_map(|(idx, _)| { // The part of `input_buffer` that was actually filled by the call // to `read` - &input[idx..min(input_size, idx + encode_in_chunks_of_size)] - }) - .map(|buffer| { + let buffer = &input[idx..min(input_size, idx + encode_in_chunks_of_size)]; + if buffer.len() < encode_in_chunks_of_size { leftover_buffer.extend(buffer); assert!(leftover_buffer.len() < encode_in_chunks_of_size); - return None; + None + } else { + Some(buffer) } - Some(buffer) }) - .for_each(|buffer| { - if let Some(read_buffer) = buffer { - // Encode data in chunks, then place it in `encoded_buffer` - assert_eq!(read_buffer.len(), encode_in_chunks_of_size); - encode_in_chunks_to_buffer( - supports_fast_decode_and_encode, - read_buffer, - &mut encoded_buffer, - ) - .unwrap(); - // Write all data in `encoded_buffer` to `output` - write_to_output( - &mut line_wrapping, - &mut encoded_buffer, - output, - false, - wrap == Some(0), - ) - .unwrap(); - } + .for_each(|read_buffer| { + // Encode data in chunks, then place it in `encoded_buffer` + assert_eq!(read_buffer.len(), encode_in_chunks_of_size); + encode_in_chunks_to_buffer( + supports_fast_decode_and_encode, + read_buffer, + &mut encoded_buffer, + ) + .unwrap(); + // Write all data in `encoded_buffer` to `output` + write_to_output( + &mut line_wrapping, + &mut encoded_buffer, + output, + false, + wrap == Some(0), + ) + .unwrap(); }); // Cleanup diff --git a/src/uu/basename/src/basename.rs b/src/uu/basename/src/basename.rs index 3ec5cb7c5b3..ee0c90ae0b8 100644 --- a/src/uu/basename/src/basename.rs +++ b/src/uu/basename/src/basename.rs @@ -80,9 +80,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("basename") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("basename")) .about(translate!("basename-about")) .override_usage(format_usage(&translate!("basename-usage"))) .infer_long_args(true) diff --git a/src/uu/cat/Cargo.toml b/src/uu/cat/Cargo.toml index 880972fa6d4..e5c8ca35b86 100644 --- a/src/uu/cat/Cargo.toml +++ b/src/uu/cat/Cargo.toml @@ -26,15 +26,21 @@ uucore = { workspace = true, features = ["fast-inc", "fs", "pipes", "signals"] } fluent = { workspace = true } [target.'cfg(unix)'.dependencies] -nix = { workspace = true } +rustix = { workspace = true, features = ["fs"] } [target.'cfg(windows)'.dependencies] winapi-util = { workspace = true } windows-sys = { workspace = true, features = ["Win32_Storage_FileSystem"] } [dev-dependencies] +divan = { workspace = true } tempfile = { workspace = true } +uucore = { workspace = true, features = ["benchmark"] } [[bin]] name = "cat" path = "src/main.rs" + +[[bench]] +name = "cat_bench" +harness = false diff --git a/src/uu/cat/benches/cat_bench.rs b/src/uu/cat/benches/cat_bench.rs new file mode 100644 index 00000000000..2e351970f78 --- /dev/null +++ b/src/uu/cat/benches/cat_bench.rs @@ -0,0 +1,19 @@ +use divan::{Bencher, black_box}; +use uu_cat::uumain; +use uucore::benchmark::{run_util_function, setup_test_file}; + +#[divan::bench(args = [10_000, 10_000_000])] +fn cat_default(bencher: Bencher, size_bytes: usize) { + let data = vec![b'a'; size_bytes]; + + let file_path = setup_test_file(&data); + let path_str = file_path.to_str().unwrap(); + + bencher.bench(|| { + black_box(run_util_function(uumain, &[path_str])); + }); +} + +fn main() { + divan::main(); +} diff --git a/src/uu/cat/src/cat.rs b/src/uu/cat/src/cat.rs index aef7d5eab50..a572e56d25a 100644 --- a/src/uu/cat/src/cat.rs +++ b/src/uu/cat/src/cat.rs @@ -84,10 +84,10 @@ enum CatError { /// Wrapper around `io::Error` #[error("{}", strip_errno(.0))] Io(#[from] io::Error), - /// Wrapper around `nix::Error` + /// Wrapper around `rustix::io::Errno` #[cfg(any(target_os = "linux", target_os = "android"))] #[error("{0}")] - Nix(#[from] nix::Error), + Rustix(#[from] rustix::io::Errno), /// Unknown file type; it's not a regular file, socket, etc. #[error("{}", translate!("cat-error-unknown-filetype", "ft_debug" => .ft_debug))] UnknownFiletype { @@ -229,26 +229,26 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { }; let show_nonprint = [ - options::SHOW_ALL.to_owned(), - options::SHOW_NONPRINTING_ENDS.to_owned(), - options::SHOW_NONPRINTING_TABS.to_owned(), - options::SHOW_NONPRINTING.to_owned(), + options::SHOW_ALL, + options::SHOW_NONPRINTING_ENDS, + options::SHOW_NONPRINTING_TABS, + options::SHOW_NONPRINTING, ] .iter() .any(|v| matches.get_flag(v)); let show_ends = [ - options::SHOW_ENDS.to_owned(), - options::SHOW_ALL.to_owned(), - options::SHOW_NONPRINTING_ENDS.to_owned(), + options::SHOW_ENDS, + options::SHOW_ALL, + options::SHOW_NONPRINTING_ENDS, ] .iter() .any(|v| matches.get_flag(v)); let show_tabs = [ - options::SHOW_ALL.to_owned(), - options::SHOW_TABS.to_owned(), - options::SHOW_NONPRINTING_TABS.to_owned(), + options::SHOW_ALL, + options::SHOW_TABS, + options::SHOW_NONPRINTING_TABS, ] .iter() .any(|v| matches.get_flag(v)); @@ -268,11 +268,11 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("cat") .version(uucore::crate_version!()) .override_usage(format_usage(&translate!("cat-usage"))) .about(translate!("cat-about")) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("cat")) .infer_long_args(true) .args_override_self(true) .arg( @@ -420,11 +420,11 @@ where Ok(()) } else { // each next line is expected to display "cat: …" - let line_joiner = format!("\n{}: ", uucore::util_name()); + let line_joiner = "\ncat: "; Err(uucore::error::USimpleError::new( error_messages.len() as i32, - error_messages.join(&line_joiner), + error_messages.join(line_joiner), )) } } @@ -478,17 +478,17 @@ fn get_input_type(path: &OsString) -> CatResult { /// simple memory copy. fn write_fast(handle: &mut InputHandle) -> CatResult<()> { let stdout = io::stdout(); - let mut stdout_lock = stdout.lock(); #[cfg(any(target_os = "linux", target_os = "android"))] { // If we're on Linux or Android, try to use the splice() system call // for faster writing. If it works, we're done. - if !splice::write_fast_using_splice(handle, &stdout_lock)? { + if !splice::write_fast_using_splice(handle, &stdout)? { return Ok(()); } } // If we're not on Linux or Android, or the splice() call failed, // fall back on slower writing. + let mut stdout_lock = stdout.lock(); let mut buf = [0; 1024 * 64]; loop { match handle.reader.read(&mut buf) { diff --git a/src/uu/cat/src/platform/mod.rs b/src/uu/cat/src/platform/mod.rs index 3fa27a27686..80ef6e233ac 100644 --- a/src/uu/cat/src/platform/mod.rs +++ b/src/uu/cat/src/platform/mod.rs @@ -9,6 +9,12 @@ pub use self::unix::is_unsafe_overwrite; #[cfg(windows)] pub use self::windows::is_unsafe_overwrite; +// WASI: no fstat-based device/inode checks available; assume safe. +#[cfg(target_os = "wasi")] +pub fn is_unsafe_overwrite(_input: &I, _output: &O) -> bool { + false +} + #[cfg(unix)] mod unix; diff --git a/src/uu/cat/src/platform/unix.rs b/src/uu/cat/src/platform/unix.rs index b2cdda2aa67..6cc55fc6967 100644 --- a/src/uu/cat/src/platform/unix.rs +++ b/src/uu/cat/src/platform/unix.rs @@ -5,8 +5,7 @@ // spell-checker:ignore lseek seekable -use nix::fcntl::{FcntlArg, OFlag, fcntl}; -use nix::unistd::{Whence, lseek}; +use rustix::fs::{OFlags, SeekFrom, fcntl_getfl}; use std::os::fd::AsFd; use uucore::fs::FileInformation; @@ -31,10 +30,10 @@ pub fn is_unsafe_overwrite(input: &I, output: &O) -> bool { if file_size == 0 { return false; } - // `lseek` returns an error if the file descriptor is closed or it refers to + // `seek` returns an error if the file descriptor is closed or it refers to // a non-seekable resource (e.g., pipe, socket, or some devices). - let input_pos = lseek(input.as_fd(), 0, Whence::SeekCur); - let output_pos = lseek(output.as_fd(), 0, Whence::SeekCur); + let input_pos = rustix::fs::seek(input, SeekFrom::Current(0)).map(|v| v as i64); + let output_pos = rustix::fs::seek(output, SeekFrom::Current(0)).map(|v| v as i64); if is_appending(output) { if let Ok(pos) = input_pos { if pos >= 0 && (pos as u64) >= file_size { @@ -54,9 +53,8 @@ pub fn is_unsafe_overwrite(input: &I, output: &O) -> bool { /// Whether the file is opened with the `O_APPEND` flag fn is_appending(file: &F) -> bool { - let flags_raw = fcntl(file.as_fd(), FcntlArg::F_GETFL).unwrap_or_default(); - let flags = OFlag::from_bits_truncate(flags_raw); - flags.contains(OFlag::O_APPEND) + let flags = fcntl_getfl(file).unwrap_or(OFlags::empty()); + flags.contains(OFlags::APPEND) } #[cfg(test)] diff --git a/src/uu/cat/src/splice.rs b/src/uu/cat/src/splice.rs index ca5265d2bf8..87cbff81a3e 100644 --- a/src/uu/cat/src/splice.rs +++ b/src/uu/cat/src/splice.rs @@ -4,12 +4,11 @@ // file that was distributed with this source code. use super::{CatResult, FdReadable, InputHandle}; -use nix::unistd; +use rustix::io::{read, write}; use std::os::{fd::AsFd, unix::io::AsRawFd}; -use uucore::pipes::{pipe, splice, splice_exact}; +use uucore::pipes::{MAX_ROOTLESS_PIPE_SIZE, pipe, splice, splice_exact}; -const SPLICE_SIZE: usize = 1024 * 128; const BUF_SIZE: usize = 1024 * 16; /// This function is called from `write_fast()` on Linux and Android. The @@ -24,48 +23,58 @@ pub(super) fn write_fast_using_splice( handle: &InputHandle, write_fd: &S, ) -> CatResult { - let (pipe_rd, pipe_wr) = pipe()?; - - loop { - match splice(&handle.reader, &pipe_wr, SPLICE_SIZE) { - Ok(n) => { - if n == 0 { - return Ok(false); - } - if splice_exact(&pipe_rd, write_fd, 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, write_fd, n)?; - return Ok(true); - } + if splice(&handle.reader, &write_fd, MAX_ROOTLESS_PIPE_SIZE).is_ok() { + // fcntl improves throughput + // todo: avoid fcntl overhead for small input, but don't fcntl inside of the loop + let _ = rustix::pipe::fcntl_setpipe_size(write_fd, MAX_ROOTLESS_PIPE_SIZE); + loop { + match splice(&handle.reader, &write_fd, MAX_ROOTLESS_PIPE_SIZE) { + Ok(1..) => {} + Ok(0) => return Ok(false), + Err(_) => return Ok(true), } - Err(_) => { - return Ok(true); + } + } else if let Ok((pipe_rd, pipe_wr)) = pipe() { + // both of in/output are not pipe. needs broker to use splice() with additional costs + loop { + match splice(&handle.reader, &pipe_wr, MAX_ROOTLESS_PIPE_SIZE) { + Ok(0) => return Ok(false), + Ok(n) => { + if splice_exact(&pipe_rd, write_fd, 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, write_fd, n)?; + return Ok(true); + } + } + Err(_) => return Ok(true), } } + } else { + Ok(true) } } /// Move exactly `num_bytes` bytes from `read_fd` to `write_fd`. /// /// Panics if not enough bytes can be read. -fn copy_exact(read_fd: &impl AsFd, write_fd: &impl AsFd, num_bytes: usize) -> nix::Result<()> { +fn copy_exact(read_fd: &impl AsFd, write_fd: &impl AsFd, num_bytes: usize) -> std::io::Result<()> { let mut left = num_bytes; let mut buf = [0; BUF_SIZE]; while left > 0 { - let read = unistd::read(read_fd, &mut buf)?; - assert_ne!(read, 0, "unexpected end of pipe"); + let n = read(read_fd, &mut buf)?; + assert_ne!(n, 0, "unexpected end of pipe"); let mut written = 0; - while written < read { - match unistd::write(write_fd, &buf[written..read])? { - 0 => panic!(), - n => written += n, + while written < n { + match write(write_fd, &buf[written..n])? { + 0 => unreachable!("fd should be writable"), + w => written += w, } } - left -= read; + left -= n; } Ok(()) } diff --git a/src/uu/chcon/src/chcon.rs b/src/uu/chcon/src/chcon.rs index 685ad0dcf0d..9953c064fd3 100644 --- a/src/uu/chcon/src/chcon.rs +++ b/src/uu/chcon/src/chcon.rs @@ -156,7 +156,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - let cmd = Command::new(uucore::util_name()) + let cmd = Command::new("chcon") .version(uucore::crate_version!()) .about(translate!("chcon-about")) .override_usage(format_usage(&translate!("chcon-usage"))) @@ -613,7 +613,7 @@ fn process_file( if options.verbose { println!( "{}", - translate!("chcon-verbose-changing-context", "util_name" => uucore::util_name(), "file" => file_full_name.quote()) + translate!("chcon-verbose-changing-context", "util_name" => "chcon", "file" => file_full_name.quote()) ); } diff --git a/src/uu/checksum_common/src/lib.rs b/src/uu/checksum_common/src/lib.rs index f0d307870e7..56ded13f19e 100644 --- a/src/uu/checksum_common/src/lib.rs +++ b/src/uu/checksum_common/src/lib.rs @@ -63,7 +63,7 @@ pub fn standalone_with_length_main( algo: AlgoKind, cmd: Command, args: impl uucore::Args, - validate_len: fn(&str) -> UResult>, + validate_len: fn(&str) -> UResult, ) -> UResult<()> { let matches = uucore::clap_localization::handle_clap_result(cmd, args)?; let algo = Some(algo); @@ -72,8 +72,7 @@ pub fn standalone_with_length_main( .get_one::(options::LENGTH) .map(String::as_str) .map(validate_len) - .transpose()? - .flatten(); + .transpose()?; //todo: deduplicate matches.get_flag let text = !matches.get_flag(options::BINARY); diff --git a/src/uu/chgrp/src/chgrp.rs b/src/uu/chgrp/src/chgrp.rs index cd890e49a6f..0e41a67601b 100644 --- a/src/uu/chgrp/src/chgrp.rs +++ b/src/uu/chgrp/src/chgrp.rs @@ -98,7 +98,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - let cmd = Command::new(uucore::util_name()) + let cmd = Command::new("chgrp") .version(uucore::crate_version!()) .about(translate!("chgrp-about")) .override_usage(format_usage(&translate!("chgrp-usage"))) diff --git a/src/uu/chmod/src/chmod.rs b/src/uu/chmod/src/chmod.rs index d77378da453..a50f0a6bf0e 100644 --- a/src/uu/chmod/src/chmod.rs +++ b/src/uu/chmod/src/chmod.rs @@ -14,7 +14,6 @@ use thiserror::Error; use uucore::display::Quotable; use uucore::error::{ExitCode, UError, UResult, USimpleError, UUsageError, set_exit_code}; use uucore::fs::display_permissions_unix; -use uucore::libc::mode_t; use uucore::mode; use uucore::perms::{TraverseSymlinks, configure_symlink_and_recursion}; @@ -175,11 +174,11 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("chmod") .version(uucore::crate_version!()) .about(translate!("chmod-about")) .override_usage(format_usage(&translate!("chmod-usage"))) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("chmod")) .args_override_self(true) .infer_long_args(true) .no_binary_name(true) @@ -313,8 +312,8 @@ impl Chmoder { /// Report permission changes based on verbose and changes flags fn report_permission_change(&self, file_path: &Path, old_mode: u32, new_mode: u32) { if self.verbose || self.changes { - let current_permissions = display_permissions_unix(old_mode as mode_t, false); - let new_permissions = display_permissions_unix(new_mode as mode_t, false); + let current_permissions = display_permissions_unix(old_mode, false); + let new_permissions = display_permissions_unix(new_mode, false); if new_mode != old_mode { println!( @@ -668,8 +667,8 @@ impl Chmoder { if (new_mode & !naively_expected_new_mode) != 0 { return Err(ChmodError::NewPermissions( file.into(), - display_permissions_unix(new_mode as mode_t, false), - display_permissions_unix(naively_expected_new_mode as mode_t, false), + display_permissions_unix(new_mode, false), + display_permissions_unix(naively_expected_new_mode, false), ) .into()); } @@ -691,8 +690,8 @@ impl Chmoder { println!( "failed to change mode of file {} from {fperm:04o} ({}) to {mode:04o} ({})", file.quote(), - display_permissions_unix(fperm as mode_t, false), - display_permissions_unix(mode as mode_t, false) + display_permissions_unix(fperm, false), + display_permissions_unix(mode, false) ); } Err(1) diff --git a/src/uu/chown/locales/en-US.ftl b/src/uu/chown/locales/en-US.ftl index 0dfe8301e9d..9bcb725a6c3 100644 --- a/src/uu/chown/locales/en-US.ftl +++ b/src/uu/chown/locales/en-US.ftl @@ -21,3 +21,6 @@ chown-error-failed-to-get-attributes = failed to get attributes of { $file } chown-error-invalid-user = invalid user: { $user } chown-error-invalid-group = invalid group: { $group } chown-error-invalid-spec = invalid spec: { $spec } + +# Warning messages +chown-warning-dot-separator = '.' should be ':': { $spec } diff --git a/src/uu/chown/locales/fr-FR.ftl b/src/uu/chown/locales/fr-FR.ftl index 48e39853a3e..deaa7a620e9 100644 --- a/src/uu/chown/locales/fr-FR.ftl +++ b/src/uu/chown/locales/fr-FR.ftl @@ -21,3 +21,6 @@ chown-error-failed-to-get-attributes = échec de l'obtention des attributs de { chown-error-invalid-user = utilisateur invalide : { $user } chown-error-invalid-group = groupe invalide : { $group } chown-error-invalid-spec = spécification invalide : { $spec } + +# Messages d'avertissement +chown-warning-dot-separator = '.' devrait être ':' : { $spec } diff --git a/src/uu/chown/src/chown.rs b/src/uu/chown/src/chown.rs index 6f0599ce811..ac71abd3804 100644 --- a/src/uu/chown/src/chown.rs +++ b/src/uu/chown/src/chown.rs @@ -9,6 +9,7 @@ use uucore::display::Quotable; pub use uucore::entries::{self, Group, Locate, Passwd}; use uucore::format_usage; use uucore::perms::{GidUidOwnerFilter, IfFrom, chown_base, options}; +use uucore::show_warning; use uucore::translate; use uucore::error::{FromIo, UResult, USimpleError}; @@ -75,9 +76,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("chown") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("chown")) .about(translate!("chown-about")) .override_usage(format_usage(&translate!("chown-usage"))) .infer_long_args(true) @@ -151,7 +152,7 @@ pub fn uu_app() -> Command { } /// Parses the user string to extract the UID. -fn parse_uid(user: &str, spec: &str, sep: char) -> UResult> { +fn parse_uid(user: &str, spec: &str) -> UResult> { if user.is_empty() { return Ok(None); } @@ -160,11 +161,6 @@ fn parse_uid(user: &str, spec: &str, sep: char) -> UResult> { return Ok(Some(u.uid)); } - // Handle `username.groupname` syntax (e.g. when sep is ':' but spec contains '.') - if spec.contains('.') && !spec.contains(':') && sep == ':' { - return parse_spec(spec, '.').map(|(uid, _)| uid); - } - // Fallback: `user` string contains a numeric user ID user.parse().map(Some).map_err(|_| { USimpleError::new( @@ -209,7 +205,20 @@ fn parse_spec(spec: &str, sep: char) -> UResult<(Option, Option)> { let user = args.next().unwrap_or(""); let group = args.next().unwrap_or(""); - let uid = parse_uid(user, spec, sep)?; + // dot separator: try as username first, fall back to owner.group (like GNU) + if sep == ':' && !spec.contains(':') && spec.contains('.') { + if let Ok(uid) = parse_uid(user, spec) { + let gid = parse_gid(group, spec)?; + return Ok((uid, gid)); + } + show_warning!( + "{}", + translate!("chown-warning-dot-separator", "spec" => spec.quote()) + ); + return parse_spec(spec, '.'); + } + + let uid = parse_uid(user, spec)?; let gid = parse_gid(group, spec)?; if user.chars().next().is_some_and(char::is_numeric) && group.is_empty() && spec != user { diff --git a/src/uu/chroot/src/chroot.rs b/src/uu/chroot/src/chroot.rs index a08e4b0cc9a..68e8fe2ed05 100644 --- a/src/uu/chroot/src/chroot.rs +++ b/src/uu/chroot/src/chroot.rs @@ -220,7 +220,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - let cmd = Command::new(uucore::util_name()) + let cmd = Command::new("chroot") .version(uucore::crate_version!()) .about(translate!("chroot-about")) .override_usage(format_usage(&translate!("chroot-usage"))) diff --git a/src/uu/cksum/benches/cksum_bench.rs b/src/uu/cksum/benches/cksum_bench.rs index 5c2d3d9c614..81f70bbb3d1 100644 --- a/src/uu/cksum/benches/cksum_bench.rs +++ b/src/uu/cksum/benches/cksum_bench.rs @@ -104,7 +104,7 @@ bench_algorithm!(cksum_sha224, "sha224"); bench_algorithm!(cksum_sha256, "sha256"); bench_algorithm!(cksum_sha384, "sha384"); bench_algorithm!(cksum_sha512, "sha512"); -// broken. benchmarking error messages issues/10002 bench_algorithm!(cksum_blake3, "blake3"); +bench_algorithm!(cksum_blake3, "blake3"); bench_shake_algorithm!(cksum_shake128, "shake128", Shake128); bench_shake_algorithm!(cksum_shake256, "shake256", Shake256); diff --git a/src/uu/cksum/src/cksum.rs b/src/uu/cksum/src/cksum.rs index e484c8323ad..df3f46a14a0 100644 --- a/src/uu/cksum/src/cksum.rs +++ b/src/uu/cksum/src/cksum.rs @@ -12,7 +12,7 @@ use uu_checksum_common::{ChecksumCommand, checksum_main, default_checksum_app, o use uucore::checksum::compute::OutputFormat; use uucore::checksum::{ - AlgoKind, ChecksumError, calculate_blake2b_length_str, sanitize_sha2_sha3_length_str, + AlgoKind, BlakeLength, ChecksumError, parse_blake_length, sanitize_sha2_sha3_length_str, }; use uucore::error::UResult; use uucore::hardware::{HasHardwareFeatures as _, SimdPolicy}; @@ -67,8 +67,10 @@ fn maybe_sanitize_length( Err(_) => Err(ChecksumError::InvalidLength(len.into()).into()), }, - // For BLAKE2b, if a length is provided, validate it. - (Some(AlgoKind::Blake2b), Some(len)) => calculate_blake2b_length_str(len), + // For BLAKE, if a length is provided, validate it. + (Some(algo @ (AlgoKind::Blake2b | AlgoKind::Blake3)), Some(len)) => { + parse_blake_length(algo, BlakeLength::String(len)).map(Some) + } // For any other provided algorithm, check if length is 0. // Otherwise, this is an error. diff --git a/src/uu/comm/src/comm.rs b/src/uu/comm/src/comm.rs index 9257c1b7d1a..395c561e10e 100644 --- a/src/uu/comm/src/comm.rs +++ b/src/uu/comm/src/comm.rs @@ -374,9 +374,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("comm") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("comm")) .about(translate!("comm-about")) .override_usage(format_usage(&translate!("comm-usage"))) .infer_long_args(true) diff --git a/src/uu/cp/locales/en-US.ftl b/src/uu/cp/locales/en-US.ftl index 36ab60f25f0..7c47a91b4d2 100644 --- a/src/uu/cp/locales/en-US.ftl +++ b/src/uu/cp/locales/en-US.ftl @@ -109,10 +109,8 @@ cp-debug-enum-seek-hole-zeros = SEEK_HOLE + zeros cp-warning-source-specified-more-than-once = source { $file_type } { $source } specified more than once # Verbose and debug messages -cp-verbose-copied = { $source } -> { $dest } cp-debug-skipped = skipped { $path } cp-verbose-removed = removed { $path } -cp-verbose-created-directory = { $source } -> { $dest } cp-debug-copy-offload = copy offload: { $offload }, reflink: { $reflink }, sparse detection: { $sparse } # Prompts diff --git a/src/uu/cp/locales/fr-FR.ftl b/src/uu/cp/locales/fr-FR.ftl index 76860de0351..b9d7450016b 100644 --- a/src/uu/cp/locales/fr-FR.ftl +++ b/src/uu/cp/locales/fr-FR.ftl @@ -44,7 +44,7 @@ cp-help-one-file-system = rester sur ce système de fichiers cp-help-sparse = contrôler la création de fichiers épars. Voir ci-dessous cp-help-selinux = définir le contexte de sécurité SELinux du fichier de destination au type par défaut cp-help-context = comme -Z, ou si CTX est spécifié, définir le contexte de sécurité SELinux ou SMACK à CTX -cp-help-progress = Afficher une barre de progression. Note : cette fonctionnalité n'est pas supportée par GNU coreutils. +cp-help-progress = Afficher une barre de progression. Note : cette fonctionnalité n'est pas prise en charge par GNU coreutils. cp-help-copy-contents = Non implémenté : copier le contenu des fichiers spéciaux lors de la récursion # Messages d'erreur @@ -78,8 +78,8 @@ cp-error-not-all-files-copied = Tous les fichiers n'ont pas été copiés cp-error-reflink-always-sparse-auto = `--reflink=always` ne peut être utilisé qu'avec --sparse=auto cp-error-file-exists = { $path } : Le fichier existe cp-error-invalid-backup-argument = --backup est mutuellement exclusif avec -n ou --update=none-fail -cp-error-reflink-not-supported = --reflink n'est supporté que sur linux et macOS -cp-error-sparse-not-supported = --sparse n'est supporté que sur linux +cp-error-reflink-not-supported = --reflink n'est pris en charge que sur linux et macOS +cp-error-sparse-not-supported = --sparse n'est pris en charge que sur linux cp-error-not-a-directory = { $path } n'est pas un répertoire cp-error-selinux-not-enabled = SELinux n'était pas activé lors de la compilation ! cp-error-selinux-set-context = échec de la définition du contexte de sécurité de { $path } : { $error } @@ -98,7 +98,7 @@ cp-error-backup-format = cp : { $error } cp-debug-enum-no = non cp-debug-enum-yes = oui cp-debug-enum-avoided = évité -cp-debug-enum-unsupported = non supporté +cp-debug-enum-unsupported = non pris en charge cp-debug-enum-unknown = inconnu cp-debug-enum-zeros = zéros cp-debug-enum-seek-hole = SEEK_HOLE @@ -108,10 +108,8 @@ cp-debug-enum-seek-hole-zeros = SEEK_HOLE + zéros cp-warning-source-specified-more-than-once = { $file_type } source { $source } spécifié plus d'une fois # Messages verbeux et de débogage -cp-verbose-copied = { $source } -> { $dest } cp-debug-skipped = { $path } ignoré -cp-verbose-removed = removed { $path } -cp-verbose-created-directory = { $source } -> { $dest } +cp-verbose-removed = supprimé { $path } cp-debug-copy-offload = copy offload : { $offload }, reflink : { $reflink }, sparse detection : { $sparse } # Invites diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index 403a8892da5..9f023a3937f 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -521,7 +521,7 @@ pub fn uu_app() -> Command { options::ATTRIBUTES_ONLY, options::COPY_CONTENTS, ]; - Command::new(uucore::util_name()) + Command::new("cp") .version(uucore::crate_version!()) .about(translate!("cp-about")) .help_template(uucore::localized_help_template(uucore::util_name())) @@ -1404,7 +1404,7 @@ pub fn copy(sources: &[PathBuf], target: &Path, options: &Options) -> CopyResult ) .unwrap(), ) - .with_message(uucore::util_name()); + .with_message("cp"); pb.tick(); Some(pb) } else { @@ -1620,7 +1620,7 @@ fn file_mode_for_interactive_overwrite( Some(( format!("{mode_without_leading_digits:04o}"), - uucore::fs::display_permissions_unix(mode, false), + uucore::fs::display_permissions_unix(mode as u32, false), )) } } @@ -1896,9 +1896,19 @@ pub(crate) fn copy_attributes( fn symlink_file( source: &Path, dest: &Path, - symlinked_files: &mut HashSet, + #[cfg(not(target_os = "wasi"))] symlinked_files: &mut HashSet, + #[cfg(target_os = "wasi")] _symlinked_files: &mut HashSet, ) -> CopyResult<()> { - #[cfg(not(windows))] + #[cfg(target_os = "wasi")] + { + Err(CpError::IoErrContext( + io::Error::new(io::ErrorKind::Unsupported, "symlinks not supported"), + translate!("cp-error-cannot-create-symlink", + "dest" => get_filename(dest).unwrap_or("?").quote(), + "source" => get_filename(source).unwrap_or("?").quote()), + )) + } + #[cfg(not(any(windows, target_os = "wasi")))] { std::os::unix::fs::symlink(source, dest).map_err(|e| { CpError::IoErrContext( @@ -1920,10 +1930,13 @@ fn symlink_file( ) })?; } - if let Ok(file_info) = FileInformation::from_path(dest, false) { - symlinked_files.insert(file_info); + #[cfg(not(target_os = "wasi"))] + { + if let Ok(file_info) = FileInformation::from_path(dest, false) { + symlinked_files.insert(file_info); + } + Ok(()) } - Ok(()) } fn context_for(src: &Path, dest: &Path) -> String { @@ -2200,10 +2213,7 @@ fn print_paths(parents: bool, source: &Path, dest: &Path) { // a/b -> d/a/b // for (x, y) in aligned_ancestors(source, dest) { - println!( - "{}", - translate!("cp-verbose-created-directory", "source" => x.display(), "dest" => y.display()) - ); + println!("{} -> {}", x.display(), y.display()); } } diff --git a/src/uu/csplit/src/csplit.rs b/src/uu/csplit/src/csplit.rs index 4445eb75668..3a4b43e881e 100644 --- a/src/uu/csplit/src/csplit.rs +++ b/src/uu/csplit/src/csplit.rs @@ -645,7 +645,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("csplit") .version(uucore::crate_version!()) .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("csplit-about")) diff --git a/src/uu/cut/locales/fr-FR.ftl b/src/uu/cut/locales/fr-FR.ftl index cab0d8ccd35..a95773099d6 100644 --- a/src/uu/cut/locales/fr-FR.ftl +++ b/src/uu/cut/locales/fr-FR.ftl @@ -70,7 +70,7 @@ cut-after-help = Chaque appel doit spécifier un mode (quoi utiliser pour les co #### Filtrage optionnel basé sur le délimiteur - Si le drapeau --only-delimited (-s) est fourni, seules les lignes qui + Si l'option --only-delimited (-s) est fournie, seules les lignes qui contiennent le délimiteur seront affichées #### Remplacer le délimiteur diff --git a/src/uu/cut/src/cut.rs b/src/uu/cut/src/cut.rs index 714e423e8d5..1e0cf90bcd8 100644 --- a/src/uu/cut/src/cut.rs +++ b/src/uu/cut/src/cut.rs @@ -260,9 +260,9 @@ fn cut_fields_newline_char_delim( reader: R, out: &mut W, ranges: &[Range], - only_delimited: bool, newline_char: u8, out_delim: &[u8], + only_delimited: bool, ) -> UResult<()> { let mut reader = BufReader::new(reader); let mut line = Vec::new(); @@ -398,9 +398,9 @@ fn cut_fields( reader, out, ranges, - field_opts.only_delimited, newline_char, out_delim, + field_opts.only_delimited, ) } Delimiter::Slice(delim) => { @@ -680,7 +680,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("cut") .version(uucore::crate_version!()) .help_template(uucore::localized_help_template(uucore::util_name())) .override_usage(format_usage(&translate!("cut-usage"))) diff --git a/src/uu/cut/src/matcher.rs b/src/uu/cut/src/matcher.rs index b4294129442..c1be9fb5ee7 100644 --- a/src/uu/cut/src/matcher.rs +++ b/src/uu/cut/src/matcher.rs @@ -27,20 +27,14 @@ impl Matcher for ExactMatcher<'_> { fn next_match(&self, haystack: &[u8]) -> Option<(usize, usize)> { let mut pos = 0usize; loop { - match memchr(self.needle[0], &haystack[pos..]) { - Some(match_idx) => { - let match_idx = match_idx + pos; // account for starting from pos - if self.needle.len() == 1 - || haystack[match_idx + 1..].starts_with(&self.needle[1..]) - { - return Some((match_idx, match_idx + self.needle.len())); - } - pos = match_idx + 1; - } - None => { - return None; - } + let match_idx = memchr(self.needle[0], &haystack[pos..])?; + let match_idx = match_idx + pos; // account for starting from pos + + if self.needle.len() == 1 || haystack[match_idx + 1..].starts_with(&self.needle[1..]) { + return Some((match_idx, match_idx + self.needle.len())); } + + pos = match_idx + 1; } } } @@ -50,19 +44,17 @@ pub struct WhitespaceMatcher {} impl Matcher for WhitespaceMatcher { fn next_match(&self, haystack: &[u8]) -> Option<(usize, usize)> { - match memchr2(b' ', b'\t', haystack) { - Some(match_idx) => { - let mut skip = match_idx + 1; - while skip < haystack.len() { - match haystack[skip] { - b' ' | b'\t' => skip += 1, - _ => break, - } - } - Some((match_idx, skip)) + let match_idx = memchr2(b' ', b'\t', haystack)?; + let mut skip = match_idx + 1; + + while skip < haystack.len() { + match haystack[skip] { + b' ' | b'\t' => skip += 1, + _ => break, } - None => None, } + + Some((match_idx, skip)) } } diff --git a/src/uu/cut/src/searcher.rs b/src/uu/cut/src/searcher.rs index dc252d804f7..a25fa7909a1 100644 --- a/src/uu/cut/src/searcher.rs +++ b/src/uu/cut/src/searcher.rs @@ -31,14 +31,11 @@ impl Iterator for Searcher<'_, '_, M> { type Item = (usize, usize); fn next(&mut self) -> Option { - match self.matcher.next_match(&self.haystack[self.position..]) { - Some((first, last)) => { - let result = (first + self.position, last + self.position); - self.position += last; - Some(result) - } - None => None, - } + let (first, last) = self.matcher.next_match(&self.haystack[self.position..])?; + let result = (first + self.position, last + self.position); + self.position += last; + + Some(result) } } diff --git a/src/uu/date/Cargo.toml b/src/uu/date/Cargo.toml index 36604d0c337..840b8602f15 100644 --- a/src/uu/date/Cargo.toml +++ b/src/uu/date/Cargo.toml @@ -44,7 +44,8 @@ regex = { workspace = true } uucore = { workspace = true, features = ["parser", "i18n-datetime"] } [target.'cfg(unix)'.dependencies] -nix = { workspace = true, features = ["time"] } +libc = { workspace = true } +rustix = { workspace = true, features = ["time"] } [target.'cfg(windows)'.dependencies] windows-sys = { workspace = true, features = [ diff --git a/src/uu/date/locales/en-US.ftl b/src/uu/date/locales/en-US.ftl index 512510c1b53..ea864904285 100644 --- a/src/uu/date/locales/en-US.ftl +++ b/src/uu/date/locales/en-US.ftl @@ -106,6 +106,7 @@ date-error-setting-date-not-supported-redox = setting the date is not supported date-error-cannot-set-date = cannot set date date-error-extra-operand = extra operand '{$operand}' date-error-write = write error: {$error} +date-error-format-modifier-width-too-large = format modifier width '{$width}' is too large for specifier '%{$specifier}' date-error-format-missing-plus = the argument {$arg} lacks a leading '+'; when using an option to specify date(s), any non-option argument must be a format string beginning with '+' diff --git a/src/uu/date/locales/fr-FR.ftl b/src/uu/date/locales/fr-FR.ftl index cf827246634..9a67704af1e 100644 --- a/src/uu/date/locales/fr-FR.ftl +++ b/src/uu/date/locales/fr-FR.ftl @@ -101,6 +101,7 @@ date-error-setting-date-not-supported-redox = la définition de la date n'est pa date-error-cannot-set-date = impossible de définir la date date-error-extra-operand = opérande supplémentaire '{$operand}' date-error-write = erreur d'écriture: {$error} +date-error-format-modifier-width-too-large = la largeur du modificateur de format '{$width}' est trop grande pour le spécificateur '%{$specifier}' date-error-format-missing-plus = l'argument {$arg} ne commence pas par un signe '+'; - lorsqu'une option est utilisée pour spécifier une ou plusieurs dates, tout argument autre + lorsqu'une option est utilisée pour spécifier une ou plusieurs dates, tout argument autre qu'une option doit être une chaîne de format commençant par un signe '+'. diff --git a/src/uu/date/src/date.rs b/src/uu/date/src/date.rs index e577d081c49..ab7cbf680c6 100644 --- a/src/uu/date/src/date.rs +++ b/src/uu/date/src/date.rs @@ -591,7 +591,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("date") .version(uucore::crate_version!()) .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("date-about")) @@ -706,26 +706,30 @@ fn format_date_with_locale_aware_months( date: &Zoned, format_string: &str, config: &Config, - skip_localization: bool, + #[cfg(feature = "i18n-datetime")] skip_localization: bool, + #[cfg(not(feature = "i18n-datetime"))] _skip_localization: bool, ) -> Result { - // First check if format string has GNU modifiers (width/flags) and format if present - // This optimization combines detection and formatting in a single pass - if let Some(result) = - format_modifiers::format_with_modifiers_if_present(date, format_string, config) - { + // Apply locale-aware name substitution (month/day names) before modifier + // processing, so that formats like "%-e" don't bypass localization of "%b"/"%A". + // The owned String is kept in `localized` so `fmt` can borrow from it for the + // rest of the function without a dangling reference. + #[cfg(feature = "i18n-datetime")] + let localized: Option = (!skip_localization && should_use_icu_locale()) + .then(|| localize_format_string(format_string, date.date())); + #[cfg(feature = "i18n-datetime")] + let fmt: &str = localized.as_deref().unwrap_or(format_string); + #[cfg(not(feature = "i18n-datetime"))] + let fmt = format_string; + + // Check if format string has GNU modifiers (width/flags) and format if present + if let Some(result) = format_modifiers::format_with_modifiers_if_present(date, fmt, config) { return result.map_err(|e| e.to_string()); } let broken_down = BrokenDownTime::from(date); - - let result = if !should_use_icu_locale() || skip_localization { - broken_down.to_string_with_config(config, format_string) - } else { - let fmt = localize_format_string(format_string, date.date()); - broken_down.to_string_with_config(config, &fmt) - }; - - result.map_err(|e| e.to_string()) + broken_down + .to_string_with_config(config, fmt) + .map_err(|e| e.to_string()) } /// Return the appropriate format string for the given settings. @@ -977,12 +981,12 @@ fn get_clock_resolution() -> Timestamp { /// as `CLOCK_REALTIME` is required to be supported. /// Failure would indicate a non-conforming or otherwise broken implementation. fn get_clock_resolution() -> Timestamp { - use nix::time::{ClockId, clock_getres}; + use rustix::time::{ClockId, clock_getres}; - let timespec = clock_getres(ClockId::CLOCK_REALTIME).unwrap(); + let timespec = clock_getres(ClockId::Realtime); - #[allow(clippy::unnecessary_cast)] // Cast required on 32-bit platforms - Timestamp::constant(timespec.tv_sec() as _, timespec.tv_nsec() as _) + #[allow(clippy::unnecessary_cast, reason = "needed for 32 bit target")] + Timestamp::constant(timespec.tv_sec as _, timespec.tv_nsec as _) } #[cfg(all(unix, target_os = "redox"))] @@ -1039,12 +1043,16 @@ fn set_system_datetime(_date: Zoned) -> UResult<()> { /// `` /// `` fn set_system_datetime(date: Zoned) -> UResult<()> { - use nix::{sys::time::TimeSpec, time::ClockId}; + use rustix::time::{ClockId, Timespec, clock_settime}; let ts = date.timestamp(); - let timespec = TimeSpec::new(ts.as_second() as _, ts.subsec_nanosecond() as _); + let timespec = Timespec { + tv_sec: ts.as_second() as _, + tv_nsec: ts.subsec_nanosecond() as _, + }; - nix::time::clock_settime(ClockId::CLOCK_REALTIME, timespec) + clock_settime(ClockId::Realtime, timespec) + .map_err(std::io::Error::from) .map_err_context(|| translate!("date-error-cannot-set-date")) } diff --git a/src/uu/date/src/format_modifiers.rs b/src/uu/date/src/format_modifiers.rs index 00e9df77817..7cdf32b174c 100644 --- a/src/uu/date/src/format_modifiers.rs +++ b/src/uu/date/src/format_modifiers.rs @@ -38,22 +38,30 @@ use jiff::fmt::strtime::{BrokenDownTime, Config, PosixCustom}; use regex::Regex; use std::fmt; use std::sync::OnceLock; +use uucore::translate; /// Error type for format modifier operations #[derive(Debug)] pub enum FormatError { /// Error from the underlying jiff library JiffError(jiff::Error), - /// Custom error message (reserved for future use) - #[allow(dead_code)] - Custom(String), + /// Field width calculation overflowed or required allocation failed + FieldWidthTooLarge { width: usize, specifier: String }, } impl fmt::Display for FormatError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::JiffError(e) => write!(f, "{e}"), - Self::Custom(s) => write!(f, "{s}"), + Self::FieldWidthTooLarge { width, specifier } => write!( + f, + "{}", + translate!( + "date-error-format-modifier-width-too-large", + "width" => width, + "specifier" => specifier + ) + ), } } } @@ -147,7 +155,7 @@ fn format_with_modifiers( // Apply modifiers to the formatted value let width: usize = width_str.parse().unwrap_or(0); let explicit_width = !width_str.is_empty(); - let modified = apply_modifiers(&formatted, flags, width, spec, explicit_width); + let modified = apply_modifiers(&formatted, flags, width, spec, explicit_width)?; result.push_str(&modified); } else { // No modifiers, use formatted value as-is @@ -266,7 +274,7 @@ fn apply_modifiers( width: usize, specifier: &str, explicit_width: bool, -) -> String { +) -> Result { let mut result = value.to_string(); // Determine default pad character based on specifier type @@ -329,14 +337,17 @@ fn apply_modifiers( .all(|c| !c.is_alphabetic() || c.is_uppercase()) { result = result.to_lowercase(); - } else { + } else if !result + .chars() + .all(|c| !c.is_alphabetic() || c.is_lowercase()) + { result = result.to_uppercase(); } } // If no_pad flag is active, suppress all padding and return if no_pad { - return strip_default_padding(&result); + return Ok(strip_default_padding(&result)); } // Handle padding flag without explicit width: use default width @@ -348,9 +359,9 @@ fn apply_modifiers( width }; - // Handle width smaller than result: strip default padding to fit + // When the requested width is narrower than the default formatted width, GNU first removes default padding and then reapplies the requested width. if effective_width > 0 && effective_width < result.len() { - return strip_default_padding(&result); + result = strip_default_padding(&result); } // Strip default padding when switching pad characters on numeric fields @@ -387,14 +398,45 @@ fn apply_modifiers( // Zero padding: sign first, then zeros (e.g., "-0022") let sign = result.chars().next().unwrap(); let rest = &result[1..]; - result = format!("{sign}{}{rest}", "0".repeat(padding)); + let mut padded = try_alloc_padded(result.len(), padding, effective_width, specifier)?; + padded.push(sign); + padded.extend(std::iter::repeat_n('0', padding)); + padded.push_str(rest); + result = padded; } else { // Default: pad on the left (e.g., " -22" or " 1999") - result = format!("{}{result}", pad_char.to_string().repeat(padding)); + let mut padded = try_alloc_padded(result.len(), padding, effective_width, specifier)?; + padded.extend(std::iter::repeat_n(pad_char, padding)); + padded.push_str(&result); + result = padded; } } - result + Ok(result) +} + +/// Allocate a `String` with enough capacity for `current_len + padding`, +/// returning `FieldWidthTooLarge` on arithmetic overflow or allocation failure. +fn try_alloc_padded( + current_len: usize, + padding: usize, + width: usize, + specifier: &str, +) -> Result { + let target_len = + current_len + .checked_add(padding) + .ok_or_else(|| FormatError::FieldWidthTooLarge { + width, + specifier: specifier.to_string(), + })?; + let mut s = String::new(); + s.try_reserve(target_len) + .map_err(|_| FormatError::FieldWidthTooLarge { + width, + specifier: specifier.to_string(), + })?; + Ok(s) } #[cfg(test)] @@ -574,63 +616,90 @@ mod tests { #[test] fn test_apply_modifiers_basic() { // No modifiers (numeric specifier) - assert_eq!(apply_modifiers("1999", "", 0, "Y", false), "1999"); + assert_eq!(apply_modifiers("1999", "", 0, "Y", false).unwrap(), "1999"); // Zero padding - assert_eq!(apply_modifiers("1999", "0", 10, "Y", true), "0000001999"); + assert_eq!( + apply_modifiers("1999", "0", 10, "Y", true).unwrap(), + "0000001999" + ); // Space padding (strips leading zeros) - assert_eq!(apply_modifiers("06", "_", 5, "m", true), " 6"); + assert_eq!(apply_modifiers("06", "_", 5, "m", true).unwrap(), " 6"); // No-pad (strips leading zeros, width ignored) - assert_eq!(apply_modifiers("01", "-", 5, "d", true), "1"); + assert_eq!(apply_modifiers("01", "-", 5, "d", true).unwrap(), "1"); // Uppercase - assert_eq!(apply_modifiers("june", "^", 0, "B", false), "JUNE"); + assert_eq!(apply_modifiers("june", "^", 0, "B", false).unwrap(), "JUNE"); // Swap case: all uppercase → lowercase - assert_eq!(apply_modifiers("UTC", "#", 0, "Z", false), "utc"); + assert_eq!(apply_modifiers("UTC", "#", 0, "Z", false).unwrap(), "utc"); // Swap case: mixed case → uppercase - assert_eq!(apply_modifiers("June", "#", 0, "B", false), "JUNE"); + assert_eq!(apply_modifiers("June", "#", 0, "B", false).unwrap(), "JUNE"); } #[test] fn test_apply_modifiers_signs() { // Force sign with explicit width - assert_eq!(apply_modifiers("1970", "+", 6, "Y", true), "+01970"); + assert_eq!( + apply_modifiers("1970", "+", 6, "Y", true).unwrap(), + "+01970" + ); // Force sign without explicit width: should NOT add sign for 4-digit year - assert_eq!(apply_modifiers("1999", "+", 0, "Y", false), "1999"); + assert_eq!(apply_modifiers("1999", "+", 0, "Y", false).unwrap(), "1999"); // Force sign without explicit width: SHOULD add sign for year > 4 digits - assert_eq!(apply_modifiers("12345", "+", 0, "Y", false), "+12345"); + assert_eq!( + apply_modifiers("12345", "+", 0, "Y", false).unwrap(), + "+12345" + ); // Negative with zero padding: sign first, then zeros - assert_eq!(apply_modifiers("-22", "0", 5, "s", true), "-0022"); + assert_eq!(apply_modifiers("-22", "0", 5, "s", true).unwrap(), "-0022"); // Negative with space padding: spaces first, then sign - assert_eq!(apply_modifiers("-22", "_", 5, "s", true), " -22"); + assert_eq!(apply_modifiers("-22", "_", 5, "s", true).unwrap(), " -22"); // Force sign (_+): + is last, overrides _ → zero pad with sign - assert_eq!(apply_modifiers("5", "_+", 5, "s", true), "+0005"); + assert_eq!(apply_modifiers("5", "_+", 5, "s", true).unwrap(), "+0005"); // No-pad + uppercase: no padding applied - assert_eq!(apply_modifiers("june", "-^", 10, "B", true), "JUNE"); + assert_eq!( + apply_modifiers("june", "-^", 10, "B", true).unwrap(), + "JUNE" + ); } #[test] fn test_case_flag_precedence() { // Test that ^ (uppercase) overrides # (swap case) - assert_eq!(apply_modifiers("June", "^#", 0, "B", false), "JUNE"); - assert_eq!(apply_modifiers("June", "#^", 0, "B", false), "JUNE"); + assert_eq!( + apply_modifiers("June", "^#", 0, "B", false).unwrap(), + "JUNE" + ); + assert_eq!( + apply_modifiers("June", "#^", 0, "B", false).unwrap(), + "JUNE" + ); // Test # alone (swap case) - assert_eq!(apply_modifiers("June", "#", 0, "B", false), "JUNE"); - assert_eq!(apply_modifiers("JUNE", "#", 0, "B", false), "june"); + assert_eq!(apply_modifiers("June", "#", 0, "B", false).unwrap(), "JUNE"); + assert_eq!(apply_modifiers("JUNE", "#", 0, "B", false).unwrap(), "june"); } #[test] fn test_apply_modifiers_text_specifiers() { // Text specifiers default to space padding - assert_eq!(apply_modifiers("June", "", 10, "B", true), " June"); - assert_eq!(apply_modifiers("Mon", "", 10, "a", true), " Mon"); + assert_eq!( + apply_modifiers("June", "", 10, "B", true).unwrap(), + " June" + ); + assert_eq!( + apply_modifiers("Mon", "", 10, "a", true).unwrap(), + " Mon" + ); // Numeric specifiers default to zero padding - assert_eq!(apply_modifiers("6", "", 10, "m", true), "0000000006"); + assert_eq!( + apply_modifiers("6", "", 10, "m", true).unwrap(), + "0000000006" + ); } #[test] fn test_apply_modifiers_width_smaller_than_result() { // Width smaller than result strips default padding - assert_eq!(apply_modifiers("01", "", 1, "d", true), "1"); - assert_eq!(apply_modifiers("06", "", 1, "m", true), "6"); + assert_eq!(apply_modifiers("01", "", 1, "d", true).unwrap(), "1"); + assert_eq!(apply_modifiers("06", "", 1, "m", true).unwrap(), "6"); } #[test] @@ -650,33 +719,50 @@ mod tests { for (value, flags, width, spec, explicit_width, expected) in test_cases { assert_eq!( - apply_modifiers(value, flags, width, spec, explicit_width), + apply_modifiers(value, flags, width, spec, explicit_width).unwrap(), expected, "value='{value}', flags='{flags}', width={width}, spec='{spec}', explicit_width={explicit_width}", ); } } + #[test] + fn test_apply_modifiers_width_too_large() { + let err = apply_modifiers("x", "", usize::MAX, "c", true).unwrap_err(); + assert!(matches!( + err, + FormatError::FieldWidthTooLarge { width, specifier } + if width == usize::MAX && specifier == "c" + )); + } + #[test] fn test_underscore_flag_without_width() { // %_m should pad month to default width 2 with spaces - assert_eq!(apply_modifiers("6", "_", 0, "m", false), " 6"); + assert_eq!(apply_modifiers("6", "_", 0, "m", false).unwrap(), " 6"); // %_d should pad day to default width 2 with spaces - assert_eq!(apply_modifiers("1", "_", 0, "d", false), " 1"); + assert_eq!(apply_modifiers("1", "_", 0, "d", false).unwrap(), " 1"); // %_H should pad hour to default width 2 with spaces - assert_eq!(apply_modifiers("5", "_", 0, "H", false), " 5"); + assert_eq!(apply_modifiers("5", "_", 0, "H", false).unwrap(), " 5"); // %_Y should pad year to default width 4 with spaces - assert_eq!(apply_modifiers("1999", "_", 0, "Y", false), "1999"); // already at default width + assert_eq!(apply_modifiers("1999", "_", 0, "Y", false).unwrap(), "1999"); + // already at default width } #[test] fn test_plus_flag_without_width() { // %+Y without width should NOT add sign for 4-digit year - assert_eq!(apply_modifiers("1999", "+", 0, "Y", false), "1999"); + assert_eq!(apply_modifiers("1999", "+", 0, "Y", false).unwrap(), "1999"); // %+Y without width SHOULD add sign for year > 4 digits - assert_eq!(apply_modifiers("12345", "+", 0, "Y", false), "+12345"); + assert_eq!( + apply_modifiers("12345", "+", 0, "Y", false).unwrap(), + "+12345" + ); // %+Y with explicit width should add sign - assert_eq!(apply_modifiers("1999", "+", 6, "Y", true), "+01999"); + assert_eq!( + apply_modifiers("1999", "+", 6, "Y", true).unwrap(), + "+01999" + ); } #[test] diff --git a/src/uu/date/src/locale.rs b/src/uu/date/src/locale.rs index 8d1c0b0133e..d5a044c734b 100644 --- a/src/uu/date/src/locale.rs +++ b/src/uu/date/src/locale.rs @@ -28,7 +28,6 @@ macro_rules! cfg_langinfo { cfg_langinfo! { use std::ffi::CStr; use std::sync::OnceLock; - use nix::libc; #[cfg(test)] use std::sync::Mutex; diff --git a/src/uu/dd/locales/fr-FR.ftl b/src/uu/dd/locales/fr-FR.ftl index 153608174eb..494a284a814 100644 --- a/src/uu/dd/locales/fr-FR.ftl +++ b/src/uu/dd/locales/fr-FR.ftl @@ -51,7 +51,7 @@ dd-after-help = ### Opérandes - noxfer : Afficher les statistiques de volume finales, mais pas les statistiques de performance. - none : N'afficher aucune statistique. - L'affichage des statistiques de performance est aussi déclenché par le signal INFO (quand supporté), + L'affichage des statistiques de performance est aussi déclenché par le signal INFO (quand pris en charge), ou le signal USR1. Définir la variable d'environnement POSIXLY_CORRECT à n'importe quelle valeur (y compris une valeur vide) fera ignorer le signal USR1. diff --git a/src/uu/dd/src/dd.rs b/src/uu/dd/src/dd.rs index e1d7b0ad6b2..677fe0908ac 100644 --- a/src/uu/dd/src/dd.rs +++ b/src/uu/dd/src/dd.rs @@ -184,13 +184,14 @@ impl Num { /// directly in `buf_size`-sized chunks, matching GNU dd's behavior. /// Returns the total number of bytes actually read. fn read_and_discard(reader: &mut R, n: u64, buf_size: usize) -> io::Result { - let mut buf = vec![0u8; buf_size]; + // todo: consider splice()ing to /dev/null on Linux + let mut buf = Vec::with_capacity(buf_size); let mut total = 0u64; let mut remaining = n; - while remaining > 0 { - let to_read = cmp::min(remaining, buf_size as u64) as usize; - match reader.read(&mut buf[..to_read]) { + let to_read = cmp::min(remaining, buf_size as u64); + buf.clear(); + match reader.by_ref().take(to_read).read_to_end(&mut buf) { Ok(0) => break, // EOF Ok(bytes_read) => { total += bytes_read as u64; @@ -1166,10 +1167,6 @@ fn dd_copy(mut i: Input, o: Output) -> io::Result<()> { ); } - // Create a common buffer with a capacity of the block size. - // This is the max size needed. - let mut buf = vec![BUF_INIT_BYTE; bsize]; - // Spawn a timer thread to provide a scheduled signal indicating when we // should send an update of our progress to the reporting thread. // @@ -1201,6 +1198,11 @@ fn dd_copy(mut i: Input, o: Output) -> io::Result<()> { BlockWriter::Unbuffered(o) }; + // Create a common empty buffer with a capacity of the block size. + // This is the max size needed. + let mut buf = Vec::new(); + buf.try_reserve(bsize)?; // try_with_capacity is unstable https://github.com/rust-lang/rust/issues/91913 + // The main read/write loop. // // Each iteration reads blocks from the input and writes @@ -1366,6 +1368,7 @@ fn read_helper(i: &mut Input, buf: &mut Vec, bsize: usize) -> io::Result UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("dd") .version(uucore::crate_version!()) .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("dd-about")) diff --git a/src/uu/dd/src/numbers.rs b/src/uu/dd/src/numbers.rs index f718052015c..36d141f66e6 100644 --- a/src/uu/dd/src/numbers.rs +++ b/src/uu/dd/src/numbers.rs @@ -5,36 +5,9 @@ //! Functions for formatting a number as a magnitude and a unit suffix. -/// The first ten powers of 1024. -const IEC_BASES: [u128; 10] = [ - 1, - 1_024, - 1_048_576, - 1_073_741_824, - 1_099_511_627_776, - 1_125_899_906_842_624, - 1_152_921_504_606_846_976, - 1_180_591_620_717_411_303_424, - 1_208_925_819_614_629_174_706_176, - 1_237_940_039_285_380_274_899_124_224, -]; +use uucore::parser::parse_size::{IEC_BASES, SI_BASES}; const IEC_SUFFIXES: [&str; 9] = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"]; - -/// The first ten powers of 1000. -const SI_BASES: [u128; 10] = [ - 1, - 1_000, - 1_000_000, - 1_000_000_000, - 1_000_000_000_000, - 1_000_000_000_000_000, - 1_000_000_000_000_000_000, - 1_000_000_000_000_000_000_000, - 1_000_000_000_000_000_000_000_000, - 1_000_000_000_000_000_000_000_000_000, -]; - const SI_SUFFIXES: [&str; 9] = ["B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; /// A `SuffixType` determines whether the suffixes are 1000 or 1024 based. diff --git a/src/uu/dd/src/parseargs.rs b/src/uu/dd/src/parseargs.rs index 42235cabda5..0df5c2b3886 100644 --- a/src/uu/dd/src/parseargs.rs +++ b/src/uu/dd/src/parseargs.rs @@ -524,8 +524,8 @@ pub fn parse_bytes_with_opt_multiplier(s: &str) -> Result { parse_bytes_no_x(s, parts[0]) } else { let mut total: u64 = 1; - for part in parts { - if part == "0" { + for (i, part) in parts.iter().enumerate() { + if *part == "0" && i != parts.len() - 1 { show_zero_multiplier_warning(); } let num = parse_bytes_no_x(s, part)?; diff --git a/src/uu/df/Cargo.toml b/src/uu/df/Cargo.toml index 9b99baa9287..dfb5aea8def 100644 --- a/src/uu/df/Cargo.toml +++ b/src/uu/df/Cargo.toml @@ -26,7 +26,7 @@ thiserror = { workspace = true } fluent = { workspace = true } [target.'cfg(unix)'.dependencies] -nix = { workspace = true, features = ["fs"] } +rustix = { workspace = true, features = ["fs"] } [dev-dependencies] divan = { workspace = true } diff --git a/src/uu/df/src/blocks.rs b/src/uu/df/src/blocks.rs index 24ebbf44225..16f0e840200 100644 --- a/src/uu/df/src/blocks.rs +++ b/src/uu/df/src/blocks.rs @@ -9,37 +9,11 @@ use std::{env, fmt}; use uucore::{ display::Quotable, - parser::parse_size::{ParseSizeError, parse_size_non_zero_u64, parse_size_u64}, + parser::parse_size::{ + IEC_BASES, ParseSizeError, SI_BASES, parse_size_non_zero_u64, parse_size_u64, + }, }; -/// The first ten powers of 1024. -const IEC_BASES: [u128; 10] = [ - 1, - 1_024, - 1_048_576, - 1_073_741_824, - 1_099_511_627_776, - 1_125_899_906_842_624, - 1_152_921_504_606_846_976, - 1_180_591_620_717_411_303_424, - 1_208_925_819_614_629_174_706_176, - 1_237_940_039_285_380_274_899_124_224, -]; - -/// The first ten powers of 1000. -const SI_BASES: [u128; 10] = [ - 1, - 1_000, - 1_000_000, - 1_000_000_000, - 1_000_000_000_000, - 1_000_000_000_000_000, - 1_000_000_000_000_000_000, - 1_000_000_000_000_000_000_000, - 1_000_000_000_000_000_000_000_000, - 1_000_000_000_000_000_000_000_000_000, -]; - /// A `SuffixType` determines whether the suffixes are 1000 or 1024 based, and whether they are /// intended for `HumanReadable` mode or not. #[derive(Clone, Copy)] @@ -50,8 +24,8 @@ pub(crate) enum SuffixType { } impl SuffixType { - /// The first ten powers of 1024 and 1000, respectively. - fn bases(self) -> [u128; 10] { + /// The first eleven powers of 1024 and 1000, respectively. + fn bases(self) -> [u128; 11] { match self { Self::Iec | Self::HumanReadable(HumanReadable::Binary) => IEC_BASES, Self::Si | Self::HumanReadable(HumanReadable::Decimal) => SI_BASES, diff --git a/src/uu/df/src/df.rs b/src/uu/df/src/df.rs index 51b950dd460..f7d5efdaaf3 100644 --- a/src/uu/df/src/df.rs +++ b/src/uu/df/src/df.rs @@ -142,7 +142,7 @@ enum OptionsError { .0.iter() .map(|t| translate!("df-error-filesystem-type-both-selected-and-excluded", "type" => t.quote())) .collect::>() - .join(format!("\n{}: ", uucore::util_name()).as_str()) + .join("\ndf: ") )] FilesystemTypeBothSelectedAndExcluded(Vec), } @@ -301,7 +301,7 @@ fn get_all_filesystems(opt: &Options) -> UResult> { // Run a sync call before any operation if so instructed. if opt.sync { #[cfg(not(any(windows, target_os = "redox")))] - nix::unistd::sync(); + rustix::fs::sync(); } let mut mounts = vec![]; @@ -451,7 +451,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { if matches.get_flag(OPT_INODES) { println!( "{}", - translate!("df-error-inodes-not-supported-windows", "program" => uucore::util_name()) + translate!("df-error-inodes-not-supported-windows", "program" => "df") ); return Ok(()); } @@ -498,7 +498,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("df") .version(uucore::crate_version!()) .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("df-about")) diff --git a/src/uu/dircolors/src/dircolors.rs b/src/uu/dircolors/src/dircolors.rs index 3eb2593a51a..154d879b671 100644 --- a/src/uu/dircolors/src/dircolors.rs +++ b/src/uu/dircolors/src/dircolors.rs @@ -238,9 +238,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("dircolors") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("dircolors")) .about(translate!("dircolors-about")) .after_help(translate!("dircolors-after-help")) .override_usage(format_usage(&translate!("dircolors-usage"))) diff --git a/src/uu/dirname/src/dirname.rs b/src/uu/dirname/src/dirname.rs index 09bf9928a3a..561ee742b0f 100644 --- a/src/uu/dirname/src/dirname.rs +++ b/src/uu/dirname/src/dirname.rs @@ -143,7 +143,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("dirname") .about(translate!("dirname-about")) .version(uucore::crate_version!()) .help_template(uucore::localized_help_template(uucore::util_name())) diff --git a/src/uu/du/locales/fr-FR.ftl b/src/uu/du/locales/fr-FR.ftl index 52b89145973..e4e55ed0f0f 100644 --- a/src/uu/du/locales/fr-FR.ftl +++ b/src/uu/du/locales/fr-FR.ftl @@ -10,7 +10,7 @@ du-after-help = Les valeurs affichées sont en unités de la première TAILLE di de 1000). Les unités peuvent être décimales, hexadécimales, octales, binaires. MOTIF permet des exclusions avancées. Par exemple, les syntaxes suivantes - sont supportées : + sont prises en charge : ? correspondra à un seul caractère { "*" } correspondra à zéro ou plusieurs caractères {"{"}a,b{"}"} correspondra à a ou b @@ -54,7 +54,7 @@ du-error-invalid-time-style = argument invalide { $style } pour 'style de temps' - 'iso' - +FORMAT (e.g., +%H:%M) pour un format de type 'date' Essayez '{ $help }' pour plus d'informations. -du-error-invalid-time-arg = les arguments 'birth' et 'creation' pour --time ne sont pas supportés sur cette plateforme. +du-error-invalid-time-arg = les arguments 'birth' et 'creation' pour --time ne sont pas pris en charge sur cette plateforme. du-error-invalid-glob = Syntaxe d'exclusion invalide : { $error } du-error-cannot-read-directory = impossible de lire le répertoire { $path } du-error-cannot-access = impossible d'accéder à { $path } diff --git a/src/uu/du/src/du.rs b/src/uu/du/src/du.rs index bb8c4d88561..7ccfdf5a228 100644 --- a/src/uu/du/src/du.rs +++ b/src/uu/du/src/du.rs @@ -1287,7 +1287,7 @@ fn parse_depth(max_depth_str: Option<&str>, summarize: bool) -> UResult Command { - Command::new(uucore::util_name()) + Command::new("du") .version(uucore::crate_version!()) .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("du-about")) diff --git a/src/uu/env/locales/en-US.ftl b/src/uu/env/locales/en-US.ftl index a460c69fba4..d2cdd15d140 100644 --- a/src/uu/env/locales/en-US.ftl +++ b/src/uu/env/locales/en-US.ftl @@ -28,7 +28,6 @@ env-error-unexpected-number = Unexpected character: '{ $char }', expected variab env-error-expected-brace-or-colon = Unexpected character: '{ $char }', expected a closing brace ('{"}"}') or colon (':') at position { $position } env-error-cannot-specify-null-with-command = cannot specify --null (-0) with command env-error-invalid-signal = { $signal }: invalid signal -env-error-config-file = { $file }: { $error } env-error-variable-name-issue = variable name issue (at { $position }): { $error } env-error-generic = Error: { $error } env-error-no-such-file = { $program }: No such file or directory diff --git a/src/uu/env/locales/fr-FR.ftl b/src/uu/env/locales/fr-FR.ftl index 2ca1968d230..78351e0da4b 100644 --- a/src/uu/env/locales/fr-FR.ftl +++ b/src/uu/env/locales/fr-FR.ftl @@ -29,7 +29,6 @@ env-error-expected-brace-or-colon = Caractère inattendu : '{ $char }', accolade env-error-cannot-specify-null-with-command = impossible de spécifier --null (-0) avec une commande env-error-invalid-signal = { $signal } : signal invalide -env-error-config-file = { $file } : { $error } env-error-variable-name-issue = problème de nom de variable (à { $position }) : { $error } env-error-generic = Erreur : { $error } env-error-no-such-file = { $program } : Aucun fichier ou répertoire de ce type @@ -38,7 +37,7 @@ env-error-cannot-unset = impossible de supprimer '{ $name }' : Argument invalide env-error-cannot-unset-invalid = impossible de supprimer { $name } : Argument invalide env-error-must-specify-command-with-chdir = doit spécifier une commande avec --chdir (-C) env-error-cannot-change-directory = impossible de changer de répertoire vers { $directory } : { $error } -env-error-argv0-not-supported = --argv0 n'est actuellement pas supporté sur cette plateforme +env-error-argv0-not-supported = --argv0 n'est actuellement pas pris en charge sur cette plateforme env-error-permission-denied = { $program } : Permission refusée env-error-unknown = erreur inconnue : { $error } env-error-failed-set-signal-action = échec de la définition de l'action du signal pour le signal { $signal } : { $error } diff --git a/src/uu/env/src/env.rs b/src/uu/env/src/env.rs index 7b36c7c6355..3343544253e 100644 --- a/src/uu/env/src/env.rs +++ b/src/uu/env/src/env.rs @@ -319,12 +319,8 @@ fn load_config_file(opts: &mut Options) -> UResult<()> { Ini::load_from_file(file) }; - let conf = conf.map_err(|e| { - USimpleError::new( - 1, - translate!("env-error-config-file", "file" => file.maybe_quote(), "error" => e), - ) - })?; + let conf = + conf.map_err(|e| USimpleError::new(1, format!("{}: {e}", file.maybe_quote())))?; for (_, prop) in &conf { // ignore all INI section lines (treat them as comments) diff --git a/src/uu/expand/src/expand.rs b/src/uu/expand/src/expand.rs index 088e8aaa363..4c3b09f64b7 100644 --- a/src/uu/expand/src/expand.rs +++ b/src/uu/expand/src/expand.rs @@ -249,7 +249,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { uucore::clap_localization::configure_localized_command( - Command::new(uucore::util_name()) + Command::new("expand") .version(uucore::crate_version!()) .about(translate!("expand-about")) .override_usage(format_usage(&translate!("expand-usage"))), diff --git a/src/uu/expr/src/expr.rs b/src/uu/expr/src/expr.rs index f2bec49e8d7..9392fcee72c 100644 --- a/src/uu/expr/src/expr.rs +++ b/src/uu/expr/src/expr.rs @@ -71,9 +71,9 @@ impl UError for ExprError { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("expr") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("expr")) .about(translate!("expr-about")) .override_usage(format_usage(&translate!("expr-usage"))) .after_help(translate!("expr-after-help")) @@ -111,12 +111,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { if args.len() == 1 && args[0] == b"--help" { uu_app().print_help()?; } else if args.len() == 1 && args[0] == b"--version" { - writeln!( - stdout(), - "{} {}", - uucore::util_name(), - uucore::crate_version!() - )?; + writeln!(stdout(), "expr {}", uucore::crate_version!())?; } else { // The first argument may be "--" and should be be ignored. let args = if !args.is_empty() && args[0] == b"--" { diff --git a/src/uu/expr/src/syntax_tree.rs b/src/uu/expr/src/syntax_tree.rs index a5c453c1745..22d0bde13d5 100644 --- a/src/uu/expr/src/syntax_tree.rs +++ b/src/uu/expr/src/syntax_tree.rs @@ -624,9 +624,6 @@ pub struct AstNode { #[derive(Debug, Clone)] #[cfg_attr(test, derive(Eq, PartialEq))] pub enum AstNodeInner { - Evaluated { - value: NumOrStr, - }, Leaf { value: MaybeNonUtf8String, }, @@ -650,15 +647,6 @@ impl AstNode { Parser::new(input).parse() } - pub fn evaluated(self) -> ExprResult { - Ok(Self { - id: get_next_id(), - inner: AstNodeInner::Evaluated { - value: self.eval()?, - }, - }) - } - pub fn eval(&self) -> ExprResult { // This function implements a recursive tree-walking algorithm, but uses an explicit // stack approach instead of native recursion to avoid potential stack overflow @@ -669,9 +657,6 @@ impl AstNode { while let Some(node) = stack.pop() { match &node.inner { - AstNodeInner::Evaluated { value, .. } => { - result_stack.insert(node.id, Ok(value.clone())); - } AstNodeInner::Leaf { value, .. } => { result_stack.insert(node.id, Ok(value.to_owned().into())); } @@ -896,9 +881,7 @@ impl<'a, S: AsRef> Parser<'a, S> { value: self.next()?.into(), }, b"(" => { - // Evaluate the node just after parsing to we detect arithmetic - // errors before checking for the closing parenthesis. - let s = self.parse_expression()?.evaluated()?; + let s = self.parse_expression()?; match self.next() { Ok(b")") => {} @@ -1070,9 +1053,7 @@ mod test { AstNode::parse(&["(", "1", "+", "2", ")", "*", "3"]), Ok(op( BinOp::Numeric(NumericOp::Mul), - op(BinOp::Numeric(NumericOp::Add), "1", "2") - .evaluated() - .unwrap(), + op(BinOp::Numeric(NumericOp::Add), "1", "2"), "3" )) ); diff --git a/src/uu/factor/src/factor.rs b/src/uu/factor/src/factor.rs index 5d381d9d393..4d3d82c52b9 100644 --- a/src/uu/factor/src/factor.rs +++ b/src/uu/factor/src/factor.rs @@ -218,7 +218,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("factor") .version(uucore::crate_version!()) .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("factor-about")) diff --git a/src/uu/fmt/locales/fr-FR.ftl b/src/uu/fmt/locales/fr-FR.ftl index 640942a8fe3..0957a973f53 100644 --- a/src/uu/fmt/locales/fr-FR.ftl +++ b/src/uu/fmt/locales/fr-FR.ftl @@ -4,7 +4,7 @@ fmt-usage = [OPTION]... [FICHIER]... # Messages d'aide fmt-crown-margin-help = La première et la deuxième ligne d'un paragraphe peuvent avoir des indentations différentes, auquel cas l'indentation de la première ligne est préservée, et chaque ligne suivante correspond à l'indentation de la deuxième ligne. fmt-tagged-paragraph-help = Comme -c, sauf que la première et la deuxième ligne d'un paragraphe *doivent* avoir des indentations différentes ou elles sont traitées comme des paragraphes séparés. -fmt-preserve-headers-help = Tente de détecter et préserver les en-têtes de courrier dans l'entrée. Attention en combinant ce drapeau avec -p. +fmt-preserve-headers-help = Tente de détecter et préserver les en-têtes de courrier dans l'entrée. Attention en combinant cette option avec -p. fmt-split-only-help = Divise les lignes seulement, ne les reformate pas. fmt-uniform-spacing-help = Insère exactement un espace entre les mots, et deux entre les phrases. Les fins de phrase dans l'entrée sont détectées comme [?!.] suivies de deux espaces ou d'une nouvelle ligne ; les autres ponctuations ne sont pas interprétées comme des fins de phrase. fmt-prefix-help = Reformate seulement les lignes commençant par PRÉFIXE, en rattachant PRÉFIXE aux lignes reformatées. À moins que -x soit spécifié, les espaces de début seront ignorés lors de la correspondance avec PRÉFIXE. diff --git a/src/uu/fmt/src/fmt.rs b/src/uu/fmt/src/fmt.rs index 42f92d6437f..5f34d062519 100644 --- a/src/uu/fmt/src/fmt.rs +++ b/src/uu/fmt/src/fmt.rs @@ -356,9 +356,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("fmt") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("fmt")) .about(translate!("fmt-about")) .override_usage(format_usage(&translate!("fmt-usage"))) .infer_long_args(true) diff --git a/src/uu/fold/src/fold.rs b/src/uu/fold/src/fold.rs index 76ef483bf31..e7b1688579b 100644 --- a/src/uu/fold/src/fold.rs +++ b/src/uu/fold/src/fold.rs @@ -82,7 +82,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("fold") .version(uucore::crate_version!()) .help_template(uucore::localized_help_template(uucore::util_name())) .override_usage(format_usage(&translate!("fold-usage"))) diff --git a/src/uu/groups/src/groups.rs b/src/uu/groups/src/groups.rs index eed278ed7f4..86128fc63b8 100644 --- a/src/uu/groups/src/groups.rs +++ b/src/uu/groups/src/groups.rs @@ -80,9 +80,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("groups") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("groups")) .about(translate!("groups-about")) .override_usage(format_usage(&translate!("groups-usage"))) .infer_long_args(true) diff --git a/src/uu/head/locales/fr-FR.ftl b/src/uu/head/locales/fr-FR.ftl index 26abb6f8b72..d5851797c26 100644 --- a/src/uu/head/locales/fr-FR.ftl +++ b/src/uu/head/locales/fr-FR.ftl @@ -2,7 +2,7 @@ head-about = Affiche les 10 premières lignes de chaque FICHIER sur la sortie st Avec plus d'un FICHIER, précède chacun d'un en-tête donnant le nom du fichier. Sans FICHIER, ou quand FICHIER est -, lit l'entrée standard. - Les arguments obligatoires pour les drapeaux longs sont obligatoires pour les drapeaux courts aussi. + Les arguments obligatoires pour les options longues sont obligatoires pour les options courtes aussi. head-usage = head [DRAPEAU]... [FICHIER]... # Messages d'aide diff --git a/src/uu/head/src/cli.rs b/src/uu/head/src/cli.rs new file mode 100644 index 00000000000..823d3869116 --- /dev/null +++ b/src/uu/head/src/cli.rs @@ -0,0 +1,84 @@ +// 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 clap::{Arg, ArgAction, Command}; +use std::ffi::OsString; +use uucore::format_usage; +use uucore::translate; + +pub mod options { + pub const BYTES: &str = "BYTES"; + pub const LINES: &str = "LINES"; + pub const QUIET: &str = "QUIET"; + pub const VERBOSE: &str = "VERBOSE"; + pub const ZERO: &str = "ZERO"; + pub const FILES: &str = "FILE"; + pub const PRESUME_INPUT_PIPE: &str = "-PRESUME-INPUT-PIPE"; +} + +pub fn uu_app() -> Command { + Command::new("head") + .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template("head")) + .about(translate!("head-about")) + .override_usage(format_usage(&translate!("head-usage"))) + .infer_long_args(true) + .arg( + Arg::new(options::BYTES) + .short('c') + .long("bytes") + .value_name("[-]NUM") + .help(translate!("head-help-bytes")) + .overrides_with_all([options::BYTES, options::LINES]) + .allow_hyphen_values(true), + ) + .arg( + Arg::new(options::LINES) + .short('n') + .long("lines") + .value_name("[-]NUM") + .help(translate!("head-help-lines")) + .overrides_with_all([options::LINES, options::BYTES]) + .allow_hyphen_values(true), + ) + .arg( + Arg::new(options::QUIET) + .short('q') + .long("quiet") + .visible_alias("silent") + .help(translate!("head-help-quiet")) + .overrides_with_all([options::VERBOSE, options::QUIET]) + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new(options::VERBOSE) + .short('v') + .long("verbose") + .help(translate!("head-help-verbose")) + .overrides_with_all([options::QUIET, options::VERBOSE]) + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new(options::PRESUME_INPUT_PIPE) + .long("presume-input-pipe") + .alias("-presume-input-pipe") + .hide(true) + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new(options::ZERO) + .short('z') + .long("zero-terminated") + .help(translate!("head-help-zero-terminated")) + .overrides_with(options::ZERO) + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new(options::FILES) + .action(ArgAction::Append) + .value_parser(clap::value_parser!(OsString)) + .value_hint(clap::ValueHint::FilePath), + ) +} diff --git a/src/uu/head/src/head.rs b/src/uu/head/src/head.rs index 428d62443a1..1fbb21dcb4c 100644 --- a/src/uu/head/src/head.rs +++ b/src/uu/head/src/head.rs @@ -5,7 +5,7 @@ // spell-checker:ignore (vars) seekable memrchr -use clap::{Arg, ArgAction, ArgMatches, Command}; +use clap::ArgMatches; use memchr::memrchr_iter; use std::ffi::OsString; use std::fs::File; @@ -13,25 +13,19 @@ use std::io::{self, BufWriter, Read, Seek, SeekFrom, Write}; use std::num::TryFromIntError; #[cfg(unix)] use std::os::fd::AsFd; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use thiserror::Error; use uucore::display::{Quotable, print_verbatim}; -use uucore::error::{FromIo, UError, UResult}; +use uucore::error::{FromIo, UError, UResult, USimpleError}; use uucore::line_ending::LineEnding; +use uucore::show; use uucore::translate; -use uucore::{format_usage, show}; const BUF_SIZE: usize = 65536; -mod options { - pub const BYTES: &str = "BYTES"; - pub const LINES: &str = "LINES"; - pub const QUIET: &str = "QUIET"; - pub const VERBOSE: &str = "VERBOSE"; - pub const ZERO: &str = "ZERO"; - pub const FILES: &str = "FILE"; - pub const PRESUME_INPUT_PIPE: &str = "-PRESUME-INPUT-PIPE"; -} +mod cli; +use crate::cli::options; +pub use crate::cli::uu_app; mod parse; mod take; @@ -66,71 +60,6 @@ impl UError for HeadError { type HeadResult = Result; -pub fn uu_app() -> Command { - Command::new(uucore::util_name()) - .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) - .about(translate!("head-about")) - .override_usage(format_usage(&translate!("head-usage"))) - .infer_long_args(true) - .arg( - Arg::new(options::BYTES) - .short('c') - .long("bytes") - .value_name("[-]NUM") - .help(translate!("head-help-bytes")) - .overrides_with_all([options::BYTES, options::LINES]) - .allow_hyphen_values(true), - ) - .arg( - Arg::new(options::LINES) - .short('n') - .long("lines") - .value_name("[-]NUM") - .help(translate!("head-help-lines")) - .overrides_with_all([options::LINES, options::BYTES]) - .allow_hyphen_values(true), - ) - .arg( - Arg::new(options::QUIET) - .short('q') - .long("quiet") - .visible_alias("silent") - .help(translate!("head-help-quiet")) - .overrides_with_all([options::VERBOSE, options::QUIET]) - .action(ArgAction::SetTrue), - ) - .arg( - Arg::new(options::VERBOSE) - .short('v') - .long("verbose") - .help(translate!("head-help-verbose")) - .overrides_with_all([options::QUIET, options::VERBOSE]) - .action(ArgAction::SetTrue), - ) - .arg( - Arg::new(options::PRESUME_INPUT_PIPE) - .long("presume-input-pipe") - .alias("-presume-input-pipe") - .hide(true) - .action(ArgAction::SetTrue), - ) - .arg( - Arg::new(options::ZERO) - .short('z') - .long("zero-terminated") - .help(translate!("head-help-zero-terminated")) - .overrides_with(options::ZERO) - .action(ArgAction::SetTrue), - ) - .arg( - Arg::new(options::FILES) - .action(ArgAction::Append) - .value_parser(clap::value_parser!(OsString)) - .value_hint(clap::ValueHint::FilePath), - ) -} - #[derive(Debug, PartialEq)] enum Mode { FirstLines(u64), @@ -510,6 +439,13 @@ fn uu_head(options: &HeadOptions) -> UResult<()> { Ok(()) } else { + if Path::new(file).is_dir() { + show!(USimpleError::new( + 1, + translate!("head-error-reading-file", "name" => file.quote(), "err" => "Is a directory") + )); + continue; + } let mut file_handle = match File::open(file) { Ok(f) => f, Err(err) => { diff --git a/src/uu/head/src/take.rs b/src/uu/head/src/take.rs index 6f05b77e595..b557036579f 100644 --- a/src/uu/head/src/take.rs +++ b/src/uu/head/src/take.rs @@ -163,6 +163,7 @@ impl TakeAllLinesBuffer { reader: &mut impl Read, separator: u8, ) -> std::io::Result { + self.partial_line = false; let bytes_read = self.inner.fill_buffer(reader)?; // Count the number of lines... self.terminated_lines = memchr_iter(separator, self.inner.remaining_buffer()).count(); diff --git a/src/uu/hostid/src/hostid.rs b/src/uu/hostid/src/hostid.rs index 041dc232344..04074b0bbda 100644 --- a/src/uu/hostid/src/hostid.rs +++ b/src/uu/hostid/src/hostid.rs @@ -21,10 +21,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { * is a no-op unless unsigned int is wider than 32 bits. */ - let mut result: c_long; - unsafe { - result = gethostid(); - } + let mut result: c_long = unsafe { gethostid() }; #[allow(overflowing_literals)] let mask = 0xffff_ffff; @@ -35,9 +32,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("hostid") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("hostid")) .about(translate!("hostid-about")) .override_usage(format_usage(&translate!("hostid-usage"))) .infer_long_args(true) diff --git a/src/uu/hostname/src/hostname.rs b/src/uu/hostname/src/hostname.rs index 70c20e780f3..53f0ac35e93 100644 --- a/src/uu/hostname/src/hostname.rs +++ b/src/uu/hostname/src/hostname.rs @@ -38,10 +38,8 @@ mod wsa { pub(super) struct WsaHandle(()); pub(super) fn start() -> io::Result { - let err = unsafe { - let mut data = std::mem::MaybeUninit::::uninit(); - WSAStartup(0x0202, data.as_mut_ptr()) - }; + let mut data = std::mem::MaybeUninit::::uninit(); + let err = unsafe { WSAStartup(0x0202, data.as_mut_ptr()) }; if err == 0 { Ok(WsaHandle(())) } else { @@ -51,10 +49,8 @@ mod wsa { impl Drop for WsaHandle { fn drop(&mut self) { - unsafe { - // This possibly returns an error but we can't handle it - let _err = WSACleanup(); - } + // This possibly returns an error but we can't handle it + let _ = unsafe { WSACleanup() }; } } } @@ -75,7 +71,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("hostname") .version(uucore::crate_version!()) .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("hostname-about")) diff --git a/src/uu/id/src/id.rs b/src/uu/id/src/id.rs index ff58c711647..78b0ad2d390 100644 --- a/src/uu/id/src/id.rs +++ b/src/uu/id/src/id.rs @@ -361,9 +361,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("id") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("id")) .about(translate!("id-about")) .override_usage(format_usage(&translate!("id-usage"))) .infer_long_args(true) diff --git a/src/uu/install/locales/en-US.ftl b/src/uu/install/locales/en-US.ftl index a9a0d5395d7..34189d86333 100644 --- a/src/uu/install/locales/en-US.ftl +++ b/src/uu/install/locales/en-US.ftl @@ -57,5 +57,4 @@ install-warning-compare-ignored = the --compare (-C) option is ignored when you install-verbose-creating-directory = creating directory { $path } install-verbose-creating-directory-step = install: creating directory { $path } install-verbose-removed = removed { $path } -install-verbose-copy = { $from } -> { $to } install-verbose-backup = (backup: { $backup }) diff --git a/src/uu/install/locales/fr-FR.ftl b/src/uu/install/locales/fr-FR.ftl index 3d5f2feddee..054cc292eb2 100644 --- a/src/uu/install/locales/fr-FR.ftl +++ b/src/uu/install/locales/fr-FR.ftl @@ -57,5 +57,4 @@ install-warning-compare-ignored = l'option --compare (-C) est ignorée quand un install-verbose-creating-directory = création du répertoire { $path } install-verbose-creating-directory-step = install : création du répertoire { $path } install-verbose-removed = supprimé { $path } -install-verbose-copy = { $from } -> { $to } install-verbose-backup = (sauvegarde : { $backup }) diff --git a/src/uu/install/src/install.rs b/src/uu/install/src/install.rs index a683ff9b908..f19f8aca249 100644 --- a/src/uu/install/src/install.rs +++ b/src/uu/install/src/install.rs @@ -70,7 +70,7 @@ pub struct Behavior { #[derive(Error, Debug)] enum InstallError { - #[error("{}", translate!("install-error-dir-needs-arg", "util_name" => uucore::util_name()))] + #[error("{}", translate!("install-error-dir-needs-arg", "util_name" => "install"))] DirNeedsArg, #[error("{}", translate!("install-error-create-dir-failed", "path" => .0.quote()))] @@ -193,9 +193,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("install") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("install")) .about(translate!("install-about")) .override_usage(format_usage(&translate!("install-usage"))) .infer_long_args(true) @@ -1106,11 +1106,7 @@ fn finalize_installed_file( } if b.verbose { - write!( - stdout(), - "{}", - translate!("install-verbose-copy", "from" => from.quote(), "to" => to.quote()) - )?; + write!(stdout(), "{} -> {}", from.quote(), to.quote())?; match backup_path { Some(path) => writeln!( stdout(), diff --git a/src/uu/join/src/join.rs b/src/uu/join/src/join.rs index 1d6801ba47f..3f95be4bc51 100644 --- a/src/uu/join/src/join.rs +++ b/src/uu/join/src/join.rs @@ -863,7 +863,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("join") .version(uucore::crate_version!()) .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("join-about")) diff --git a/src/uu/kill/src/kill.rs b/src/uu/kill/src/kill.rs index 81186b5cb5a..e92a47da3b4 100644 --- a/src/uu/kill/src/kill.rs +++ b/src/uu/kill/src/kill.rs @@ -100,9 +100,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("kill") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("kill")) .about(translate!("kill-about")) .override_usage(format_usage(&translate!("kill-usage"))) .infer_long_args(true) diff --git a/src/uu/link/src/link.rs b/src/uu/link/src/link.rs index 12ebbbee916..7af8dd0905c 100644 --- a/src/uu/link/src/link.rs +++ b/src/uu/link/src/link.rs @@ -34,9 +34,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("link") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("link")) .about(translate!("link-about")) .override_usage(format_usage(&translate!("link-usage"))) .infer_long_args(true) diff --git a/src/uu/ln/src/ln.rs b/src/uu/ln/src/ln.rs index 1c7d40b8476..5fb75a86730 100644 --- a/src/uu/ln/src/ln.rs +++ b/src/uu/ln/src/ln.rs @@ -143,9 +143,9 @@ pub fn uu_app() -> Command { backup_control::BACKUP_CONTROL_LONG_HELP ); - Command::new(uucore::util_name()) + Command::new("ln") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("ln")) .about(translate!("ln-about")) .override_usage(format_usage(&translate!("ln-usage"))) .infer_long_args(true) @@ -488,3 +488,11 @@ pub fn symlink, P2: AsRef>(src: P1, dst: P2) -> std::io::R symlink_file(src, dst) } } + +#[cfg(target_os = "wasi")] +fn symlink, P2: AsRef>(_src: P1, _dst: P2) -> std::io::Result<()> { + Err(std::io::Error::new( + std::io::ErrorKind::Unsupported, + "symlinks not supported on this platform", + )) +} diff --git a/src/uu/logname/src/logname.rs b/src/uu/logname/src/logname.rs index 32b0c85105c..07ac97be637 100644 --- a/src/uu/logname/src/logname.rs +++ b/src/uu/logname/src/logname.rs @@ -12,13 +12,11 @@ use uucore::translate; use uucore::{error::UResult, show_error}; fn get_userlogin() -> Option { - unsafe { - let login: *const libc::c_char = libc::getlogin(); - if login.is_null() { - None - } else { - Some(String::from_utf8_lossy(CStr::from_ptr(login).to_bytes()).to_string()) - } + let login_ptr = unsafe { libc::getlogin() }; + if login_ptr.is_null() { + None + } else { + Some(String::from_utf8_lossy(unsafe { CStr::from_ptr(login_ptr) }.to_bytes()).to_string()) } } @@ -36,9 +34,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("logname") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("logname")) .override_usage(translate!("logname-usage")) .about(translate!("logname-about")) .infer_long_args(true) diff --git a/src/uu/ls/Cargo.toml b/src/uu/ls/Cargo.toml index a82ad13c397..b8688819ef8 100644 --- a/src/uu/ls/Cargo.toml +++ b/src/uu/ls/Cargo.toml @@ -24,7 +24,6 @@ path = "src/ls.rs" ansi-width = { workspace = true } clap = { workspace = true, features = ["env"] } glob = { workspace = true } -hostname = { workspace = true } lscolors = { workspace = true } rustc-hash = { workspace = true } selinux = { workspace = true, optional = true } @@ -46,6 +45,10 @@ uucore = { workspace = true, features = [ uutils_term_grid = { workspace = true } fluent = { workspace = true } +# hostname crate does not support WASI (no OS-level hostname API) +[target.'cfg(not(target_os = "wasi"))'.dependencies] +hostname = { workspace = true } + [[bin]] name = "ls" path = "src/main.rs" diff --git a/src/uu/ls/locales/en-US.ftl b/src/uu/ls/locales/en-US.ftl index 7de4a17ce2e..fd6fc38fd59 100644 --- a/src/uu/ls/locales/en-US.ftl +++ b/src/uu/ls/locales/en-US.ftl @@ -22,6 +22,7 @@ ls-error-cannot-open-directory-bad-descriptor = cannot open directory {$path}: B ls-error-unknown-io-error = unknown io error: {$path}, '{$error}' ls-error-invalid-block-size = invalid --block-size argument {$size} ls-error-dired-and-zero-incompatible = --dired and --zero are incompatible +ls-error-not-directory = cannot access {$path}: Not a directory ls-error-not-listing-already-listed = {$path}: not listing already-listed directory ls-error-invalid-time-style = invalid --time-style argument {$style} Possible values are: @@ -55,7 +56,7 @@ ls-help-escape-quoting-style = Use escape quoting style. Equivalent to `--quotin ls-help-c-quoting-style = Use C quoting style. Equivalent to `--quoting-style=c` ls-help-replace-control-chars = Replace control characters with '?' if they are not escaped. ls-help-show-control-chars = Show control characters 'as is' if they are not escaped. -ls-help-show-time-field = Show time in : +ls-help-show-time-field = Show time in ``: access time (-u): atime, access, use; change time (-t): ctime, status. modification time: mtime, modification. @@ -71,7 +72,7 @@ ls-help-time-access = If the long listing format (e.g., -l, -o) is being used, p ls-help-hide-pattern = do not list implied entries matching shell PATTERN (overridden by -a or -A) ls-help-ignore-pattern = do not list implied entries matching shell PATTERN ls-help-ignore-backups = Ignore entries which end with ~. -ls-help-sort-by-field = Sort by : name, none (-U), time (-t), size (-S), extension (-X) or width +ls-help-sort-by-field = Sort by ``: name, none (-U), time (-t), size (-S), extension (-X) or width ls-help-sort-by-size = Sort by file size, largest first. ls-help-sort-by-time = Sort by modification time (the 'mtime' in the inode), newest first. ls-help-sort-by-version = Natural sort of (version) numbers in the filenames. diff --git a/src/uu/ls/locales/fr-FR.ftl b/src/uu/ls/locales/fr-FR.ftl index bf2b97f9f22..06090f76393 100644 --- a/src/uu/ls/locales/fr-FR.ftl +++ b/src/uu/ls/locales/fr-FR.ftl @@ -55,7 +55,7 @@ ls-help-escape-quoting-style = Utiliser le style de citation d'échappement. Éq ls-help-c-quoting-style = Utiliser le style de citation C. Équivalent à `--quoting-style=c` ls-help-replace-control-chars = Remplacer les caractères de contrôle par '?' s'ils ne sont pas échappés. ls-help-show-control-chars = Afficher les caractères de contrôle 'tels quels' s'ils ne sont pas échappés. -ls-help-show-time-field = Afficher l'heure dans : +ls-help-show-time-field = Afficher l'heure dans `` : heure d'accès (-u) : atime, access, use ; heure de changement (-t) : ctime, status. heure de modification : mtime, modification. @@ -71,7 +71,7 @@ ls-help-time-access = Si le format de liste long (par ex., -l, -o) est utilisé, ls-help-hide-pattern = ne pas lister les entrées implicites correspondant au MOTIF shell (surchargé par -a ou -A) ls-help-ignore-pattern = ne pas lister les entrées implicites correspondant au MOTIF shell ls-help-ignore-backups = Ignorer les entrées qui se terminent par ~. -ls-help-sort-by-field = Trier par : name, none (-U), time (-t), size (-S), extension (-X) ou width +ls-help-sort-by-field = Trier par `` : name, none (-U), time (-t), size (-S), extension (-X) ou width ls-help-sort-by-size = Trier par taille de fichier, le plus grand en premier. ls-help-sort-by-time = Trier par heure de modification (le 'mtime' dans l'inode), le plus récent en premier. ls-help-sort-by-version = Tri naturel des numéros (de version) dans les noms de fichiers. @@ -85,7 +85,7 @@ ls-help-dereference-dir-args = Ne pas suivre les liens symboliques sauf quand il donnés comme arguments de ligne de commande. ls-help-dereference-args = Ne pas suivre les liens symboliques sauf quand ils sont donnés comme arguments de ligne de commande. ls-help-no-group = Ne pas afficher le groupe en format long. -ls-help-author = Afficher l'auteur en format long. Sur les plateformes supportées, +ls-help-author = Afficher l'auteur en format long. Sur les plateformes prises en charge, l'auteur correspond toujours au propriétaire du fichier. ls-help-all-files = Ne pas ignorer les fichiers cachés (fichiers dont les noms commencent par '.'). ls-help-almost-all = Dans un répertoire, ne pas ignorer tous les noms de fichiers qui commencent par '.', diff --git a/src/uu/ls/src/colors.rs b/src/uu/ls/src/colors.rs index 197fd2c8cec..a50418cd987 100644 --- a/src/uu/ls/src/colors.rs +++ b/src/uu/ls/src/colors.rs @@ -530,7 +530,7 @@ pub(crate) fn color_name( let has_capabilities = style_manager .colors .has_explicit_style_for(Indicator::Capabilities) - && uucore::fsxattr::has_security_cap_acl(path.p_buf.as_path()); + && uucore::fsxattr::has_security_cap_acl(&path.p_buf); // If the file has capabilities, use a specific style for `ca` (capabilities) if has_capabilities { @@ -786,7 +786,7 @@ fn parse_indicator_codes() -> (FxHashMap, bool) { } fn canonicalize_indicator_value(value: &str) -> Cow<'_, str> { - if value.len() == 1 && value.chars().all(|c| c.is_ascii_digit()) { + if value.len() == 1 && value.as_bytes()[0].is_ascii_digit() { let mut canonical = String::with_capacity(2); canonical.push('0'); canonical.push_str(value); diff --git a/src/uu/ls/src/config.rs b/src/uu/ls/src/config.rs new file mode 100644 index 00000000000..e2e797f1628 --- /dev/null +++ b/src/uu/ls/src/config.rs @@ -0,0 +1,1080 @@ +// 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 (ToDO) somegroup nlink tabsize dired subdired dtype colorterm stringly +// spell-checker:ignore nohash strtime clocale + +use std::{ + borrow::Cow, + ffi::{OsStr, OsString}, + io::{IsTerminal, stdout}, + num::IntErrorKind, +}; + +use glob::Pattern; +use lscolors::LsColors; +use term_grid::SPACES_IN_TAB; + +use uucore::{ + display::Quotable, error::UResult, format::human::SizeFormat, fsext::MetadataTimeField, + line_ending::LineEnding, parser::parse_glob, parser::parse_size::parse_size_non_zero_u64, + quoting_style::QuotingStyle, show_error, show_warning, time::format, translate, +}; + +use crate::{ + LsError, + colors::{LsColorsParseError, validate_ls_colors_env}, + dired::is_dired_arg_present, + display::{Format, IndicatorStyle, LocaleQuoting, LongFormat}, + options::QUOTING_STYLE, +}; + +pub mod options { + pub mod format { + pub static ONE_LINE: &str = "1"; + pub static LONG: &str = "long"; + pub static COLUMNS: &str = "C"; + pub static ACROSS: &str = "x"; + pub static TAB_SIZE: &str = "tabsize"; + pub static COMMAS: &str = "m"; + pub static LONG_NO_OWNER: &str = "g"; + pub static LONG_NO_GROUP: &str = "o"; + pub static LONG_NUMERIC_UID_GID: &str = "numeric-uid-gid"; + } + + pub mod files { + pub static ALL: &str = "all"; + pub static ALMOST_ALL: &str = "almost-all"; + pub static UNSORTED_ALL: &str = "f"; + } + + pub mod sort { + pub static SIZE: &str = "S"; + pub static TIME: &str = "t"; + pub static NONE: &str = "U"; + pub static VERSION: &str = "v"; + pub static EXTENSION: &str = "X"; + } + + pub mod time { + pub static ACCESS: &str = "u"; + pub static CHANGE: &str = "c"; + } + + pub mod size { + pub static ALLOCATION_SIZE: &str = "size"; + pub static BLOCK_SIZE: &str = "block-size"; + pub static HUMAN_READABLE: &str = "human-readable"; + pub static SI: &str = "si"; + pub static KIBIBYTES: &str = "kibibytes"; + } + + pub mod quoting { + pub static ESCAPE: &str = "escape"; + pub static LITERAL: &str = "literal"; + pub static C: &str = "quote-name"; + } + + pub mod indicator_style { + pub static SLASH: &str = "p"; + pub static FILE_TYPE: &str = "file-type"; + pub static CLASSIFY: &str = "classify"; + } + + pub mod dereference { + pub static ALL: &str = "dereference"; + pub static ARGS: &str = "dereference-command-line"; + pub static DIR_ARGS: &str = "dereference-command-line-symlink-to-dir"; + } + + pub static HELP: &str = "help"; + pub static QUOTING_STYLE: &str = "quoting-style"; + pub static HIDE_CONTROL_CHARS: &str = "hide-control-chars"; + pub static SHOW_CONTROL_CHARS: &str = "show-control-chars"; + pub static WIDTH: &str = "width"; + pub static AUTHOR: &str = "author"; + pub static NO_GROUP: &str = "no-group"; + pub static FORMAT: &str = "format"; + pub static SORT: &str = "sort"; + pub static TIME: &str = "time"; + pub static IGNORE_BACKUPS: &str = "ignore-backups"; + pub static DIRECTORY: &str = "directory"; + pub static INODE: &str = "inode"; + pub static REVERSE: &str = "reverse"; + pub static RECURSIVE: &str = "recursive"; + pub static COLOR: &str = "color"; + pub static PATHS: &str = "paths"; + pub static INDICATOR_STYLE: &str = "indicator-style"; + pub static TIME_STYLE: &str = "time-style"; + pub static FULL_TIME: &str = "full-time"; + pub static HIDE: &str = "hide"; + pub static IGNORE: &str = "ignore"; + pub static CONTEXT: &str = "context"; + pub static GROUP_DIRECTORIES_FIRST: &str = "group-directories-first"; + pub static ZERO: &str = "zero"; + pub static DIRED: &str = "dired"; + pub static HYPERLINK: &str = "hyperlink"; +} + +const DEFAULT_TERM_WIDTH: u16 = 80; +const POSIXLY_CORRECT_BLOCK_SIZE: u64 = 512; +const DEFAULT_BLOCK_SIZE: u64 = 1024; +const DEFAULT_FILE_SIZE_BLOCK_SIZE: u64 = 1; + +pub(crate) enum Dereference { + None, + DirArgs, + Args, + All, +} + +#[derive(PartialEq, Eq)] +pub(crate) enum Sort { + None, + Name, + Size, + Time, + Version, + Extension, + Width, +} + +#[derive(PartialEq, Eq)] +pub(crate) enum Files { + All, + AlmostAll, + Normal, +} + +pub struct Config { + // Dir and vdir needs access to this field + pub format: Format, + pub(crate) files: Files, + pub(crate) sort: Sort, + pub(crate) recursive: bool, + pub(crate) reverse: bool, + pub(crate) dereference: Dereference, + pub(crate) ignore_patterns: Vec, + pub(crate) size_format: SizeFormat, + pub(crate) directory: bool, + pub(crate) time: MetadataTimeField, + #[cfg(unix)] + pub(crate) inode: bool, + pub(crate) color: Option, + pub(crate) long: LongFormat, + pub(crate) alloc_size: bool, + pub(crate) file_size_block_size: u64, + #[allow(dead_code)] + pub(crate) block_size: u64, // is never read on Windows + pub(crate) width: u16, + // Dir and vdir needs access to this field + pub quoting_style: QuotingStyle, + pub(crate) locale_quoting: Option, + pub(crate) indicator_style: IndicatorStyle, + pub(crate) time_format_recent: String, // Time format for recent dates + pub(crate) time_format_older: Option, // Time format for older dates (optional, if not present, time_format_recent is used) + pub(crate) context: bool, + #[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))] + pub(crate) selinux_supported: bool, + #[cfg(all(feature = "smack", target_os = "linux"))] + pub(crate) smack_supported: bool, + pub(crate) group_directories_first: bool, + pub(crate) line_ending: LineEnding, + pub(crate) dired: bool, + pub(crate) hyperlink: bool, + pub(crate) tab_size: usize, +} + +/// Extracts the format to display the information based on the options provided. +/// +/// # Returns +/// +/// A tuple containing the Format variant and an Option containing a &'static str +/// which corresponds to the option used to define the format. +fn extract_format(options: &clap::ArgMatches) -> (Format, Option<&'static str>) { + if let Some(format_) = options.get_one::(options::FORMAT) { + ( + match format_.as_str() { + "long" | "verbose" => Format::Long, + "single-column" => Format::OneLine, + "columns" | "vertical" => Format::Columns, + "across" | "horizontal" => Format::Across, + "commas" => Format::Commas, + // below should never happen as clap already restricts the values. + _ => unreachable!("Invalid field for --format"), + }, + Some(options::FORMAT), + ) + } else if options.get_flag(options::format::LONG) { + (Format::Long, Some(options::format::LONG)) + } else if options.get_flag(options::format::ACROSS) { + (Format::Across, Some(options::format::ACROSS)) + } else if options.get_flag(options::format::COMMAS) { + (Format::Commas, Some(options::format::COMMAS)) + } else if options.get_flag(options::format::COLUMNS) { + (Format::Columns, Some(options::format::COLUMNS)) + } else if stdout().is_terminal() { + (Format::Columns, None) + } else { + (Format::OneLine, None) + } +} + +/// Extracts the type of files to display +/// +/// # Returns +/// +/// A Files variant representing the type of files to display. +fn extract_files(options: &clap::ArgMatches) -> Files { + let get_last_index = |flag: &str| -> usize { + if options.value_source(flag) == Some(clap::parser::ValueSource::CommandLine) { + options.index_of(flag).unwrap_or(0) + } else { + 0 + } + }; + + let all_index = get_last_index(options::files::ALL); + let almost_all_index = get_last_index(options::files::ALMOST_ALL); + let unsorted_all_index = get_last_index(options::files::UNSORTED_ALL); + + let max_index = all_index.max(almost_all_index).max(unsorted_all_index); + + if max_index == 0 { + Files::Normal + } else if max_index == almost_all_index { + Files::AlmostAll + } else { + // Either -a or -f wins, both show all files + Files::All + } +} + +/// Extracts the sorting method to use based on the options provided. +/// +/// # Returns +/// +/// A Sort variant representing the sorting method to use. +fn extract_sort(options: &clap::ArgMatches) -> Sort { + let get_last_index = |flag: &str| -> usize { + if options.value_source(flag) == Some(clap::parser::ValueSource::CommandLine) { + options.index_of(flag).unwrap_or(0) + } else { + 0 + } + }; + + let sort_index = options + .get_one::(options::SORT) + .and_then(|_| options.indices_of(options::SORT)) + .map_or(0, |mut indices| indices.next_back().unwrap_or(0)); + let time_index = get_last_index(options::sort::TIME); + let size_index = get_last_index(options::sort::SIZE); + let none_index = get_last_index(options::sort::NONE); + let version_index = get_last_index(options::sort::VERSION); + let extension_index = get_last_index(options::sort::EXTENSION); + let unsorted_all_index = get_last_index(options::files::UNSORTED_ALL); + + let max_sort_index = sort_index + .max(time_index) + .max(size_index) + .max(none_index) + .max(version_index) + .max(extension_index) + .max(unsorted_all_index); + + match max_sort_index { + 0 => { + // No sort flags specified, use default behavior + if !options.get_flag(options::format::LONG) + && (options.get_flag(options::time::ACCESS) + || options.get_flag(options::time::CHANGE) + || options.get_one::(options::TIME).is_some()) + { + Sort::Time + } else { + Sort::Name + } + } + idx if idx == unsorted_all_index || idx == none_index => Sort::None, + idx if idx == sort_index => { + if let Some(field) = options.get_one::(options::SORT) { + match field.as_str() { + "none" => Sort::None, + "name" => Sort::Name, + "time" => Sort::Time, + "size" => Sort::Size, + "version" => Sort::Version, + "extension" => Sort::Extension, + "width" => Sort::Width, + _ => unreachable!("Invalid field for --sort"), + } + } else { + Sort::Name + } + } + idx if idx == time_index => Sort::Time, + idx if idx == size_index => Sort::Size, + idx if idx == version_index => Sort::Version, + idx if idx == extension_index => Sort::Extension, + _ => Sort::Name, + } +} + +/// Extracts the time to use based on the options provided. +/// +/// # Returns +/// +/// A `MetadataTimeField` variant representing the time to use. +fn extract_time(options: &clap::ArgMatches) -> MetadataTimeField { + if let Some(field) = options.get_one::(options::TIME) { + field.as_str().into() + } else if options.get_flag(options::time::ACCESS) { + MetadataTimeField::Access + } else if options.get_flag(options::time::CHANGE) { + MetadataTimeField::Change + } else { + MetadataTimeField::Modification + } +} + +/// Some env variables can be passed +/// For now, we are only verifying if empty or not and known for `TERM` +fn is_color_compatible_term() -> bool { + let term = std::env::var_os("TERM"); + let colorterm = std::env::var_os("COLORTERM"); + + // Search function in the TERM struct to manage the wildcards + let term_matches = |term: &OsStr| -> bool { + uucore::colors::TERMS.iter().any(|&pattern| { + term == pattern + || (pattern.ends_with('*') + && term + .as_encoded_bytes() + .starts_with(&pattern.as_bytes()[..pattern.len() - 1])) + }) + }; + + match (term, colorterm) { + (Some(t), Some(c)) if t.is_empty() && c.is_empty() => false, + (Some(t), _) if !t.is_empty() => term_matches(&t), + _ => true, + } +} + +/// Extracts the color option to use based on the options provided. +/// +/// # Returns +/// +/// A boolean representing whether or not to use color. +fn extract_color(options: &clap::ArgMatches) -> bool { + if !is_color_compatible_term() { + return false; + } + + let get_last_index = |flag: &str| -> usize { + if options.value_source(flag) == Some(clap::parser::ValueSource::CommandLine) { + options.index_of(flag).unwrap_or(0) + } else { + 0 + } + }; + + let color_index = options + .get_one::(options::COLOR) + .and_then(|_| options.indices_of(options::COLOR)) + .map_or(0, |mut indices| indices.next_back().unwrap_or(0)); + let unsorted_all_index = get_last_index(options::files::UNSORTED_ALL); + + let color_enabled = match options.get_one::(options::COLOR) { + None => options.contains_id(options::COLOR), + Some(val) => match val.as_str() { + "" | "always" | "yes" | "force" => true, + "auto" | "tty" | "if-tty" => stdout().is_terminal(), + /* "never" | "no" | "none" | */ _ => false, + }, + }; + + // If --color was explicitly specified, always honor it regardless of -f + // Otherwise, if -f is present without explicit color, disable color + if color_index > 0 { + // Color was explicitly specified + color_enabled + } else if unsorted_all_index > 0 { + // -f present without explicit color, disable implicit color + false + } else { + color_enabled + } +} + +/// Extracts the hyperlink option to use based on the options provided. +/// +/// # Returns +/// +/// A boolean representing whether to hyperlink files. +fn extract_hyperlink(options: &clap::ArgMatches) -> bool { + let hyperlink = options + .get_one::(options::HYPERLINK) + .unwrap() + .as_str(); + + match hyperlink { + "always" | "yes" | "force" => true, + "auto" | "tty" | "if-tty" => stdout().is_terminal(), + "never" | "no" | "none" => false, + _ => unreachable!("should be handled by clap"), + } +} + +/// Match the argument given to --quoting-style or the [`QUOTING_STYLE`] env variable. +/// +/// # Arguments +/// +/// * `style`: the actual argument string +/// * `show_control` - A boolean value representing whether to show control characters. +/// +/// # Returns +/// +/// * An option with None if the style string is invalid, or a `QuotingStyle` wrapped in `Some`. +struct QuotingStyleSpec { + style: QuotingStyle, + fixed_control: bool, + locale: Option, +} + +impl QuotingStyleSpec { + fn new(style: QuotingStyle) -> Self { + Self { + style, + fixed_control: false, + locale: None, + } + } + + fn with_locale(style: QuotingStyle, locale: LocaleQuoting) -> Self { + Self { + style, + fixed_control: true, + locale: Some(locale), + } + } +} +fn match_quoting_style_name( + style: &str, + show_control: bool, +) -> Option<(QuotingStyle, Option)> { + let spec = match style { + "literal" => QuotingStyleSpec::new(QuotingStyle::Literal { + show_control: false, + }), + "shell" => QuotingStyleSpec::new(QuotingStyle::SHELL), + "shell-always" => QuotingStyleSpec::new(QuotingStyle::SHELL_QUOTE), + "shell-escape" => QuotingStyleSpec::new(QuotingStyle::SHELL_ESCAPE), + "shell-escape-always" => QuotingStyleSpec::new(QuotingStyle::SHELL_ESCAPE_QUOTE), + "c" => QuotingStyleSpec::new(QuotingStyle::C_DOUBLE), + "escape" => QuotingStyleSpec::new(QuotingStyle::C_NO_QUOTES), + "locale" => QuotingStyleSpec { + style: QuotingStyle::Literal { + show_control: false, + }, + fixed_control: true, + locale: Some(LocaleQuoting::Single), + }, + "clocale" => QuotingStyleSpec::with_locale(QuotingStyle::C_DOUBLE, LocaleQuoting::Double), + _ => return None, + }; + + let style = if spec.fixed_control { + spec.style + } else { + spec.style.show_control(show_control) + }; + + Some((style, spec.locale)) +} + +/// Extracts the quoting style to use based on the options provided. +/// If no options are given, it looks if a default quoting style is provided +/// through the [`QUOTING_STYLE`] environment variable. +/// +/// # Arguments +/// +/// * `options` - A reference to a [`clap::ArgMatches`] object containing command line arguments. +/// * `show_control` - A boolean value representing whether or not to show control characters. +/// +/// # Returns +/// +/// A [`QuotingStyle`] variant representing the quoting style to use. +fn extract_quoting_style( + options: &clap::ArgMatches, + show_control: bool, +) -> (QuotingStyle, Option) { + let opt_quoting_style = options.get_one::(QUOTING_STYLE); + + if let Some(style) = opt_quoting_style { + match match_quoting_style_name(style, show_control) { + Some(pair) => pair, + None => unreachable!("Should have been caught by Clap"), + } + } else if options.get_flag(options::quoting::LITERAL) { + (QuotingStyle::Literal { show_control }, None) + } else if options.get_flag(options::quoting::ESCAPE) { + (QuotingStyle::C_NO_QUOTES, None) + } else if options.get_flag(options::quoting::C) { + (QuotingStyle::C_DOUBLE, None) + } else if options.get_flag(options::DIRED) { + (QuotingStyle::Literal { show_control }, None) + } else { + // If set, the QUOTING_STYLE environment variable specifies a default style. + if let Ok(style) = std::env::var("QUOTING_STYLE") { + match match_quoting_style_name(style.as_str(), show_control) { + Some(pair) => return pair, + None => eprintln!( + "{}", + translate!("ls-invalid-quoting-style", "program" => std::env::args().next().unwrap_or_else(|| "ls".to_string()), "style" => style.clone()) + ), + } + } + + // By default, `ls` uses Shell escape quoting style when writing to a terminal file + // descriptor and Literal otherwise. + if stdout().is_terminal() { + (QuotingStyle::SHELL_ESCAPE.show_control(show_control), None) + } else { + (QuotingStyle::Literal { show_control }, None) + } + } +} + +/// Extracts the indicator style to use based on the options provided. +/// +/// # Returns +/// +/// An [`IndicatorStyle`] variant representing the indicator style to use. +fn extract_indicator_style(options: &clap::ArgMatches) -> IndicatorStyle { + if let Some(field) = options.get_one::(options::INDICATOR_STYLE) { + match field.as_str() { + "none" => IndicatorStyle::None, + "file-type" => IndicatorStyle::FileType, + "classify" => IndicatorStyle::Classify, + "slash" => IndicatorStyle::Slash, + &_ => IndicatorStyle::None, + } + } else if let Some(field) = options.get_one::(options::indicator_style::CLASSIFY) { + match field.as_str() { + "never" | "no" | "none" => IndicatorStyle::None, + "always" | "yes" | "force" => IndicatorStyle::Classify, + "auto" | "tty" | "if-tty" => { + if stdout().is_terminal() { + IndicatorStyle::Classify + } else { + IndicatorStyle::None + } + } + &_ => IndicatorStyle::None, + } + } else if options.get_flag(options::indicator_style::SLASH) { + IndicatorStyle::Slash + } else if options.get_flag(options::indicator_style::FILE_TYPE) { + IndicatorStyle::FileType + } else { + IndicatorStyle::None + } +} + +/// Parses the width value from either the command line arguments or the environment variables. +fn parse_width(width_match: Option<&String>) -> Result { + let parse_width_from_args = |s: &str| -> Result { + let radix = if s.starts_with('0') && s.len() > 1 { + 8 + } else { + 10 + }; + match u16::from_str_radix(s, radix) { + Ok(x) => Ok(x), + Err(e) => match e.kind() { + IntErrorKind::PosOverflow => Ok(u16::MAX), + _ => Err(LsError::InvalidLineWidth(s.into())), + }, + } + }; + + let parse_width_from_env = |columns: OsString| { + if let Some(columns) = columns.to_str().and_then(|s| s.parse().ok()) { + columns + } else { + show_error!( + "{}", + translate!("ls-invalid-columns-width", "width" => columns.quote()) + ); + DEFAULT_TERM_WIDTH + } + }; + + let calculate_term_size = || match terminal_size::terminal_size() { + Some((width, _)) => width.0, + None => DEFAULT_TERM_WIDTH, + }; + + let ret = match width_match { + Some(x) => parse_width_from_args(x)?, + None => match std::env::var_os("COLUMNS") { + Some(columns) => parse_width_from_env(columns), + None => calculate_term_size(), + }, + }; + + Ok(ret) +} + +impl Config { + #[allow(clippy::cognitive_complexity)] + pub fn from(options: &clap::ArgMatches) -> UResult { + let context = options.get_flag(options::CONTEXT); + let (mut format, opt) = extract_format(options); + let files = extract_files(options); + + // The -o, -n and -g options are tricky. They cannot override with each + // other because it's possible to combine them. For example, the option + // -og should hide both owner and group. Furthermore, they are not + // reset if -l or --format=long is used. So these should just show the + // group: -gl or "-g --format=long". Finally, they are also not reset + // when switching to a different format option in-between like this: + // -ogCl or "-og --format=vertical --format=long". + // + // -1 has a similar issue: it does nothing if the format is long. This + // actually makes it distinct from the --format=singe-column option, + // which always applies. + // + // The idea here is to not let these options override with the other + // options, but manually whether they have an index that's greater than + // the other format options. If so, we set the appropriate format. + if format != Format::Long { + let idx = opt + .and_then(|opt| options.indices_of(opt).map(|x| x.max().unwrap())) + .unwrap_or(0); + if [ + options::format::LONG_NO_OWNER, + options::format::LONG_NO_GROUP, + options::format::LONG_NUMERIC_UID_GID, + options::FULL_TIME, + ] + .iter() + .filter_map(|opt| { + if options.value_source(opt) == Some(clap::parser::ValueSource::CommandLine) { + options.indices_of(opt) + } else { + None + } + }) + .flatten() + .any(|i| i >= idx) + { + format = Format::Long; + } else if let Some(mut indices) = options.indices_of(options::format::ONE_LINE) { + if options.value_source(options::format::ONE_LINE) + == Some(clap::parser::ValueSource::CommandLine) + && indices.any(|i| i > idx) + { + format = Format::OneLine; + } + } + } + + let sort = extract_sort(options); + let time = extract_time(options); + let mut needs_color = extract_color(options); + let hyperlink = extract_hyperlink(options); + + let opt_block_size = options.get_one::(options::size::BLOCK_SIZE); + let opt_si = opt_block_size.is_some_and(|x| x == options::size::SI) + || options.get_flag(options::size::SI); + let opt_hr = opt_block_size.is_some_and(|x| x == options::size::HUMAN_READABLE) + || options.get_flag(options::size::HUMAN_READABLE); + let opt_kb = options.get_flag(options::size::KIBIBYTES); + + let size_format = if opt_si { + SizeFormat::Decimal + } else if opt_hr { + SizeFormat::Binary + } else { + SizeFormat::Bytes + }; + + let env_var_blocksize = std::env::var_os("BLOCKSIZE"); + let env_var_block_size = std::env::var_os("BLOCK_SIZE"); + let env_var_ls_block_size = std::env::var_os("LS_BLOCK_SIZE"); + let env_var_posixly_correct = std::env::var_os("POSIXLY_CORRECT"); + let mut is_env_var_blocksize = false; + + let raw_block_size = if let Some(opt_block_size) = opt_block_size { + OsString::from(opt_block_size) + } else if let Some(env_var_ls_block_size) = env_var_ls_block_size { + env_var_ls_block_size + } else if let Some(env_var_block_size) = env_var_block_size { + env_var_block_size + } else if let Some(env_var_blocksize) = env_var_blocksize { + is_env_var_blocksize = true; + env_var_blocksize + } else { + OsString::from("") + }; + + let (file_size_block_size, block_size) = if !opt_si && !opt_hr && !raw_block_size.is_empty() + { + if let Ok(size) = parse_size_non_zero_u64(&raw_block_size.to_string_lossy()) { + match (is_env_var_blocksize, opt_kb) { + (true, true) => (DEFAULT_FILE_SIZE_BLOCK_SIZE, DEFAULT_BLOCK_SIZE), + (true, false) => (DEFAULT_FILE_SIZE_BLOCK_SIZE, size), + (false, true) => { + // --block-size overrides -k + if opt_block_size.is_some() { + (size, size) + } else { + (size, DEFAULT_BLOCK_SIZE) + } + } + (false, false) => (size, size), + } + } else { + // only fail if invalid block size was specified with --block-size, + // ignore invalid block size from env vars + if let Some(invalid_block_size) = opt_block_size { + return Err(Box::new(LsError::BlockSizeParseError( + invalid_block_size.clone(), + ))); + } + if is_env_var_blocksize { + (DEFAULT_FILE_SIZE_BLOCK_SIZE, DEFAULT_BLOCK_SIZE) + } else { + (DEFAULT_BLOCK_SIZE, DEFAULT_BLOCK_SIZE) + } + } + } else if env_var_posixly_correct.is_some() { + if opt_kb { + (DEFAULT_FILE_SIZE_BLOCK_SIZE, DEFAULT_BLOCK_SIZE) + } else { + (DEFAULT_FILE_SIZE_BLOCK_SIZE, POSIXLY_CORRECT_BLOCK_SIZE) + } + } else if opt_si { + (DEFAULT_FILE_SIZE_BLOCK_SIZE, 1000) + } else { + (DEFAULT_FILE_SIZE_BLOCK_SIZE, DEFAULT_BLOCK_SIZE) + }; + + let long = { + let author = options.get_flag(options::AUTHOR); + let group = !options.get_flag(options::NO_GROUP) + && !options.get_flag(options::format::LONG_NO_GROUP); + let owner = !options.get_flag(options::format::LONG_NO_OWNER); + #[cfg(unix)] + let numeric_uid_gid = options.get_flag(options::format::LONG_NUMERIC_UID_GID); + LongFormat { + author, + group, + owner, + #[cfg(unix)] + numeric_uid_gid, + } + }; + let width = parse_width(options.get_one::(options::WIDTH))?; + + #[allow(clippy::needless_bool)] + let mut show_control = if options.get_flag(options::HIDE_CONTROL_CHARS) { + false + } else if options.get_flag(options::SHOW_CONTROL_CHARS) { + true + } else { + !stdout().is_terminal() + }; + + let (mut quoting_style, mut locale_quoting) = extract_quoting_style(options, show_control); + let indicator_style = extract_indicator_style(options); + // Only parse the value to "--time-style" if it will become relevant. + let dired = options.get_flag(options::DIRED); + let (time_format_recent, time_format_older) = if format == Format::Long || dired { + parse_time_style(options)? + } else { + Default::default() + }; + + let mut ignore_patterns: Vec = Vec::new(); + + if options.get_flag(options::IGNORE_BACKUPS) { + ignore_patterns.push(Pattern::new("*~").unwrap()); + ignore_patterns.push(Pattern::new(".*~").unwrap()); + } + + for pattern in options + .get_many::(options::IGNORE) + .into_iter() + .flatten() + { + if let Ok(p) = parse_glob::from_str(pattern) { + ignore_patterns.push(p); + } else { + show_warning!( + "{}", + translate!("ls-invalid-ignore-pattern", "pattern" => pattern.quote()) + ); + } + } + + if files == Files::Normal { + for pattern in options + .get_many::(options::HIDE) + .into_iter() + .flatten() + { + if let Ok(p) = parse_glob::from_str(pattern) { + ignore_patterns.push(p); + } else { + show_warning!( + "{}", + translate!("ls-invalid-hide-pattern", "pattern" => pattern.quote()) + ); + } + } + } + + // According to ls info page, `--zero` implies the following flags: + // - `--show-control-chars` + // - `--format=single-column` + // - `--color=none` + // - `--quoting-style=literal` + // Current GNU ls implementation allows `--zero` Behavior to be + // overridden by later flags. + let zero_formats_opts = [ + options::format::ACROSS, + options::format::COLUMNS, + options::format::COMMAS, + options::format::LONG, + options::format::LONG_NO_GROUP, + options::format::LONG_NO_OWNER, + options::format::LONG_NUMERIC_UID_GID, + options::format::ONE_LINE, + options::FORMAT, + ]; + let zero_colors_opts = [options::COLOR]; + let zero_show_control_opts = [options::HIDE_CONTROL_CHARS, options::SHOW_CONTROL_CHARS]; + let zero_quoting_style_opts = [ + QUOTING_STYLE, + options::quoting::C, + options::quoting::ESCAPE, + options::quoting::LITERAL, + ]; + let get_last = |flag: &str| -> usize { + if options.value_source(flag) == Some(clap::parser::ValueSource::CommandLine) { + options.index_of(flag).unwrap_or(0) + } else { + 0 + } + }; + if get_last(options::ZERO) + > zero_formats_opts + .into_iter() + .map(get_last) + .max() + .unwrap_or(0) + { + format = if format == Format::Long { + format + } else { + Format::OneLine + }; + } + if get_last(options::ZERO) + > zero_colors_opts + .into_iter() + .map(get_last) + .max() + .unwrap_or(0) + { + needs_color = false; + } + if get_last(options::ZERO) + > zero_show_control_opts + .into_iter() + .map(get_last) + .max() + .unwrap_or(0) + { + show_control = true; + } + if get_last(options::ZERO) + > zero_quoting_style_opts + .into_iter() + .map(get_last) + .max() + .unwrap_or(0) + { + quoting_style = QuotingStyle::Literal { show_control }; + locale_quoting = None; + } + + if needs_color { + if let Err(err) = validate_ls_colors_env() { + if let LsColorsParseError::UnrecognizedPrefix(prefix) = &err { + show_warning!( + "{}", + translate!( + "ls-warning-unrecognized-ls-colors-prefix", + "prefix" => prefix.quote() + ) + ); + } + show_warning!("{}", translate!("ls-warning-unparsable-ls-colors")); + needs_color = false; + } + } + + let color = if needs_color { + Some(LsColors::from_env().unwrap_or_default()) + } else { + None + }; + + if dired || is_dired_arg_present() { + // --dired implies --format=long + // if we have --dired --hyperlink, we don't show dired but we still want to see the + // long format + format = Format::Long; + } + if dired && options.get_flag(options::ZERO) { + return Err(Box::new(LsError::DiredAndZeroAreIncompatible)); + } + + let dereference = if options.get_flag(options::dereference::ALL) { + Dereference::All + } else if options.get_flag(options::dereference::ARGS) { + Dereference::Args + } else if options.get_flag(options::dereference::DIR_ARGS) { + Dereference::DirArgs + } else if options.get_flag(options::DIRECTORY) + || indicator_style == IndicatorStyle::Classify + || format == Format::Long + { + Dereference::None + } else { + Dereference::DirArgs + }; + + let tab_size = if needs_color { + Some(0) + } else { + options + .get_one::(options::format::TAB_SIZE) + .and_then(|size| size.parse::().ok()) + .or_else(|| std::env::var("TABSIZE").ok().and_then(|s| s.parse().ok())) + } + .unwrap_or(SPACES_IN_TAB); + + Ok(Self { + format, + files, + sort, + recursive: options.get_flag(options::RECURSIVE), + reverse: options.get_flag(options::REVERSE), + dereference, + ignore_patterns, + size_format, + directory: options.get_flag(options::DIRECTORY), + time, + color, + #[cfg(unix)] + inode: options.get_flag(options::INODE), + long, + alloc_size: options.get_flag(options::size::ALLOCATION_SIZE), + file_size_block_size, + block_size, + width, + quoting_style, + locale_quoting, + indicator_style, + time_format_recent, + time_format_older, + context, + #[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))] + selinux_supported: uucore::selinux::is_selinux_enabled(), + #[cfg(all(feature = "smack", target_os = "linux"))] + smack_supported: uucore::smack::is_smack_enabled(), + group_directories_first: options.get_flag(options::GROUP_DIRECTORIES_FIRST), + line_ending: LineEnding::from_zero_flag(options.get_flag(options::ZERO)), + dired, + hyperlink, + tab_size, + }) + } +} + +fn parse_time_style(options: &clap::ArgMatches) -> Result<(String, Option), LsError> { + // TODO: Using correct locale string is not implemented. + const LOCALE_FORMAT: (&str, Option<&str>) = ("%b %e %H:%M", Some("%b %e %Y")); + + // Convert time_styles references to owned String/option. + #[expect(clippy::unnecessary_wraps, reason = "internal result helper")] + fn ok((recent, older): (&str, Option<&str>)) -> Result<(String, Option), LsError> { + Ok((recent.to_string(), older.map(String::from))) + } + + if let Some(field) = options + .get_one::(options::TIME_STYLE) + .map(Cow::from) + .or_else(|| std::env::var("TIME_STYLE").ok().map(Cow::from)) + { + //If both FULL_TIME and TIME_STYLE are present + //The one added last is dominant + if options.get_flag(options::FULL_TIME) + && options.indices_of(options::FULL_TIME).unwrap().next_back() + > options.indices_of(options::TIME_STYLE).unwrap().next_back() + { + ok((format::FULL_ISO, None)) + } else { + let field = if let Some(field) = field.strip_prefix("posix-") { + // See GNU documentation, set format to "locale" if LC_TIME="POSIX", + // else just strip the prefix and continue (even "posix+FORMAT" is + // supported). + // TODO: This needs to be moved to uucore and handled by icu? + if std::env::var_os("LC_TIME").as_deref() == Some(OsStr::new("POSIX")) + || std::env::var_os("LC_ALL").as_deref() == Some(OsStr::new("POSIX")) + { + return ok(LOCALE_FORMAT); + } + field + } else { + &field + }; + + match field { + "full-iso" => ok((format::FULL_ISO, None)), + "long-iso" => ok((format::LONG_ISO, None)), + // ISO older format needs extra padding. + "iso" => Ok(( + "%m-%d %H:%M".to_string(), + Some(format::ISO.to_string() + " "), + )), + "locale" => ok(LOCALE_FORMAT), + _ => match field.chars().next().unwrap() { + '+' => { + // recent/older formats are (optionally) separated by a newline + let mut it = field[1..].split('\n'); + let recent = it.next().unwrap_or_default(); + let older = it.next(); + match it.next() { + None => ok((recent, older)), + Some(_) => Err(LsError::TimeStyleParseError(String::from(field))), + } + } + _ => Err(LsError::TimeStyleParseError(String::from(field))), + }, + } + } + } else if options.get_flag(options::FULL_TIME) { + ok((format::FULL_ISO, None)) + } else { + ok(LOCALE_FORMAT) + } +} diff --git a/src/uu/ls/src/dired.rs b/src/uu/ls/src/dired.rs index 4d96c4d9c27..e3cda96b54f 100644 --- a/src/uu/ls/src/dired.rs +++ b/src/uu/ls/src/dired.rs @@ -108,7 +108,7 @@ pub fn print_dired_output( } /// Helper function to print positions with a given prefix. -fn print_positions(prefix: &str, positions: &Vec) { +fn print_positions(prefix: &str, positions: &[BytePosition]) { print!("{prefix}"); for c in positions { print!(" {c}"); @@ -117,16 +117,7 @@ fn print_positions(prefix: &str, positions: &Vec) { } pub fn add_total(dired: &mut DiredOutput, total_len: usize) { - if dired.padding == 0 { - // when dealing with " total: xx", it isn't part of the //DIRED// - // so, we just keep the size line to add it to the position of the next file - dired.padding = total_len + DIRED_TRAILING_OFFSET; - } else { - // += because if we are in -R, we have " dir:\n total X". So, we need to take the - // previous padding too. - // and we already have the previous position in mind - dired.padding += total_len + DIRED_TRAILING_OFFSET; - } + dired.padding += total_len + DIRED_TRAILING_OFFSET; } // when using -R, we have the dirname. we need to add it to the padding @@ -156,7 +147,7 @@ pub fn update_positions(dired: &mut DiredOutput, start: usize, end: usize, line_ start: start + padding, end: end + padding, }); - dired.line_offset = dired.line_offset + padding + line_len; + dired.line_offset += padding + line_len; // Remove the previous padding dired.padding = 0; } diff --git a/src/uu/ls/src/display.rs b/src/uu/ls/src/display.rs new file mode 100644 index 00000000000..d1c35e45c7a --- /dev/null +++ b/src/uu/ls/src/display.rs @@ -0,0 +1,1355 @@ +// 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 (ToDO) somegroup nlink tabsize dired subdired dtype colorterm stringly +// spell-checker:ignore nohash strtime clocale ilog + +use core::ops::RangeInclusive; +use std::cell::LazyCell; +#[cfg(unix)] +use std::fmt::Display; +#[cfg(unix)] +use std::os::unix::fs::{FileTypeExt, MetadataExt}; +#[cfg(windows)] +use std::os::windows::fs::MetadataExt; +use std::sync::LazyLock; +use std::time::SystemTime; +/// Show the directory name in the case where several arguments are given to ls +use std::{borrow::Cow, iter}; +use std::{ + ffi::{OsStr, OsString}, + fmt::Write as FmtWrite, + fs::{self, DirEntry, FileType, Metadata}, + io::{BufWriter, Stdout, Write}, +}; + +use ansi_width::ansi_width; +use glob::MatchOptions; +#[cfg(unix)] +use rustc_hash::FxHashMap; +use term_grid::{DEFAULT_SEPARATOR_SIZE, Direction, Filling, Grid, GridOptions}; + +#[cfg(unix)] +use uucore::entries; +#[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] +use uucore::fsxattr::has_acl; +#[cfg(any( + target_os = "linux", + target_os = "macos", + target_os = "android", + target_os = "ios", + target_os = "freebsd", + target_os = "dragonfly", + target_os = "netbsd", + target_os = "openbsd", + target_os = "illumos", + target_os = "solaris" +))] +use uucore::libc::{dev_t, major, minor}; +use uucore::{ + error::UResult, + format::human::human_readable, + fs::display_permissions, + fsext::metadata_get_time, + os_str_as_bytes_lossy, + quoting_style::{QuotingStyle, locale_aware_escape_dir_name, locale_aware_escape_name}, + show, + time::{FormatSystemTimeFallback, format_system_time}, +}; + +use crate::colors::{StyleManager, color_name}; +use crate::config::Files; +use crate::dired::{self, DiredOutput}; +use crate::{Config, ListState, LsError, PathData, get_block_size}; + +// Fields that can be removed or added to the long format +pub(crate) struct LongFormat { + pub(crate) author: bool, + pub(crate) group: bool, + pub(crate) owner: bool, + #[cfg(unix)] + pub(crate) numeric_uid_gid: bool, +} + +pub(crate) struct PaddingCollection { + #[cfg(unix)] + pub(crate) inode: usize, + pub(crate) link_count: usize, + pub(crate) uname: usize, + pub(crate) group: usize, + pub(crate) context: usize, + pub(crate) size: usize, + #[cfg(unix)] + pub(crate) major: usize, + #[cfg(unix)] + pub(crate) minor: usize, + pub(crate) block_size: usize, +} + +pub(crate) struct DisplayItemName { + pub(crate) displayed: OsString, + pub(crate) dired_name_len: usize, +} + +#[derive(PartialEq, Eq)] +pub(crate) enum IndicatorStyle { + None, + Slash, + FileType, + Classify, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +pub(crate) enum LocaleQuoting { + Single, + Double, +} + +#[derive(PartialEq, Eq, Debug)] +pub enum Format { + Columns, + Long, + OneLine, + Across, + Commas, +} + +#[allow(dead_code)] +enum SizeOrDeviceId { + Size(String), + Device(String, String), +} + +/// or the recursive flag is passed. +/// +/// ```no-exec +/// $ ls -R +/// .: <- This is printed by this function +/// dir1 file1 file2 +/// +/// dir1: <- This as well +/// file11 +/// ``` +pub fn show_dir_name( + path_data: &PathData, + out: &mut BufWriter, + config: &Config, +) -> std::io::Result<()> { + let escaped_name = escape_dir_name_with_locale(path_data.path().as_os_str(), config); + + let name = if config.hyperlink && !config.dired { + create_hyperlink(&escaped_name, path_data) + } else { + escaped_name + }; + + write_os_str(out, &name)?; + write!(out, ":") +} + +fn escape_with_locale(name: &OsStr, config: &Config, fallback: F) -> OsString +where + F: FnOnce(&OsStr, QuotingStyle) -> OsString, +{ + if let Some(locale) = config.locale_quoting { + locale_quote(name, locale) + } else { + fallback(name, config.quoting_style) + } +} + +fn escape_dir_name_with_locale(name: &OsStr, config: &Config) -> OsString { + escape_with_locale(name, config, locale_aware_escape_dir_name) +} + +fn escape_name_with_locale(name: &OsStr, config: &Config) -> OsString { + escape_with_locale(name, config, locale_aware_escape_name) +} + +fn locale_quote(name: &OsStr, style: LocaleQuoting) -> OsString { + let bytes = os_str_as_bytes_lossy(name); + let mut quoted = String::with_capacity(name.len() + 2); + match style { + LocaleQuoting::Single => quoted.push('\''), + LocaleQuoting::Double => quoted.push('"'), + } + for &byte in bytes.as_ref() { + push_locale_byte(&mut quoted, byte, style); + } + match style { + LocaleQuoting::Single => quoted.push('\''), + LocaleQuoting::Double => quoted.push('"'), + } + OsString::from(quoted) +} + +fn push_locale_byte(buf: &mut String, byte: u8, style: LocaleQuoting) { + match (style, byte) { + (LocaleQuoting::Single, b'\'') => buf.push_str("'\\''"), + (LocaleQuoting::Double, b'"') => buf.push_str("\\\""), + (_, b'\\') => buf.push_str("\\\\"), + _ => push_basic_escape(buf, byte), + } +} + +fn push_basic_escape(buf: &mut String, byte: u8) { + match byte { + b'\x07' => buf.push_str("\\a"), + b'\x08' => buf.push_str("\\b"), + b'\t' => buf.push_str("\\t"), + b'\n' => buf.push_str("\\n"), + b'\x0b' => buf.push_str("\\v"), + b'\x0c' => buf.push_str("\\f"), + b'\r' => buf.push_str("\\r"), + b'\x1b' => buf.push_str("\\e"), + b'"' => buf.push('"'), + b'\'' => buf.push('\''), + b if (0x20..=0x7e).contains(&b) => buf.push(b as char), + _ => { + let _ = write!(buf, "\\{byte:03o}"); + } + } +} + +pub fn should_display(entry: &DirEntry, config: &Config) -> bool { + // check if hidden + if config.files == Files::Normal && is_hidden(entry) { + return false; + } + + // check if it is among ignore_patterns + let options = MatchOptions { + // setting require_literal_leading_dot to match behavior in GNU ls + require_literal_leading_dot: true, + require_literal_separator: false, + case_sensitive: true, + }; + + let file_name = entry.file_name(); + // 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) => Cow::Borrowed(s), + None => file_name.to_string_lossy(), + }; + + !config + .ignore_patterns + .iter() + .any(|p| p.matches_with(&file_name, options)) +} + +fn display_dir_entry_size( + entry: &PathData, + config: &Config, + state: &mut ListState, +) -> (usize, usize, usize, usize, usize, usize) { + // TODO: Cache/memorize the display_* results so we don't have to recalculate them. + if let Some(md) = entry.metadata() { + let (size_len, major_len, minor_len) = match display_len_or_rdev(md, config) { + SizeOrDeviceId::Device(major, minor) => { + (major.len() + minor.len() + 2usize, major.len(), minor.len()) + } + SizeOrDeviceId::Size(size) => (size.len(), 0usize, 0usize), + }; + #[cfg(unix)] + let nlink_len = digits(md.nlink()); + #[cfg(not(unix))] + let nlink_len = display_symlink_count(md).len(); + ( + nlink_len, + display_uname(md, config, &mut state.uid_cache).len(), + display_group(md, config, &mut state.gid_cache).len(), + size_len, + major_len, + minor_len, + ) + } else { + (0, 0, 0, 0, 0, 0) + } +} + +#[cfg(unix)] +fn digits(num: u64) -> usize { + (num.checked_ilog10().unwrap_or(0) + 1) as usize +} + +// A simple, performant, ExtendPad trait to add a string to a Vec, padding with spaces +// on the left or right, without making additional copies, or using formatting functions. +pub trait ExtendPad { + fn extend_pad_left(&mut self, string: &str, count: usize); + fn extend_pad_right(&mut self, string: &str, count: usize); +} + +impl ExtendPad for Vec { + fn extend_pad_left(&mut self, string: &str, count: usize) { + if string.len() < count { + self.extend(iter::repeat_n(b' ', count - string.len())); + } + self.extend(string.as_bytes()); + } + + fn extend_pad_right(&mut self, string: &str, count: usize) { + self.extend(string.as_bytes()); + if string.len() < count { + self.extend(iter::repeat_n(b' ', count - string.len())); + } + } +} + +// TODO: Consider converting callers to use ExtendPad instead, as it avoids +// additional copies. +fn pad_left(string: &str, count: usize) -> String { + format!("{string:>count$}") +} + +#[allow(clippy::cognitive_complexity)] +pub fn display_items( + items: &[PathData], + config: &Config, + state: &mut ListState, + dired: &mut DiredOutput, +) -> UResult<()> { + // `-Z`, `--context`: + // Display the SELinux security context or '?' if none is found. When used with the `-l` + // option, print the security context to the left of the size column. + + let quoted = items.iter().any(|item| { + let name = escape_name_with_locale(item.display_name(), config); + os_str_starts_with(&name, b"'") + }); + + if config.format == Format::Long { + let padding_collection = calculate_padding_collection(items, config, state); + + for item in items { + #[cfg(unix)] + let should_display_leading_info = config.inode || config.alloc_size; + #[cfg(not(unix))] + let should_display_leading_info = config.alloc_size; + + if should_display_leading_info { + display_additional_leading_info(item, &padding_collection, config, &mut state.out)?; + } + + display_item_long(item, &padding_collection, config, state, dired, quoted)?; + } + } else { + let mut longest_context_len = 1; + let prefix_context = if config.context { + for item in items { + let context_len = item.security_context(config).len(); + longest_context_len = context_len.max(longest_context_len); + } + Some(longest_context_len) + } else { + None + }; + + let padding = calculate_padding_collection(items, config, state); + + // we need to apply normal color to non filename output + if let Some(style_manager) = &mut state.style_manager { + write!(state.out, "{}", style_manager.apply_normal())?; + } + + let mut names_vec = Vec::with_capacity(items.len()); + + #[cfg(unix)] + let should_display_leading_info = config.inode || config.alloc_size; + #[cfg(not(unix))] + let should_display_leading_info = config.alloc_size; + + for i in items { + let more_info = if should_display_leading_info { + let mut s = Vec::new(); + display_additional_leading_info(i, &padding, config, &mut s)?; + Some(String::from_utf8(s).unwrap()) // Should always be UTF-8 + } else { + None + }; + // it's okay to set current column to zero which is used to decide + // whether text will wrap or not, because when format is grid or + // column ls will try to place the item name in a new line if it + // wraps. + let cell = display_item_name( + i, + config, + prefix_context, + more_info, + state.style_manager.as_mut(), + LazyCell::new(|| 0), + ); + + names_vec.push(cell.displayed); + } + + let mut names = names_vec.into_iter(); + + match config.format { + Format::Columns => { + display_grid( + names, + config.width, + Direction::TopToBottom, + &mut state.out, + quoted, + config.tab_size, + )?; + } + Format::Across => { + display_grid( + names, + config.width, + Direction::LeftToRight, + &mut state.out, + quoted, + config.tab_size, + )?; + } + Format::Commas => { + let mut current_col = 0; + if let Some(name) = names.next() { + write_os_str(&mut state.out, &name)?; + current_col = ansi_width(&name.to_string_lossy()) as u16 + 2; + } + for name in names { + 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; + writeln!(state.out, ",")?; + } else { + current_col += name_width + 2; + write!(state.out, ", ")?; + } + write_os_str(&mut state.out, &name)?; + } + // Current col is never zero again if names have been printed. + // So we print a newline. + if current_col > 0 { + write!(state.out, "{}", config.line_ending)?; + } + } + _ => { + for name in names { + write_os_str(&mut state.out, &name)?; + write!(state.out, "{}", config.line_ending)?; + } + } + } + } + + Ok(()) +} + +fn display_grid( + names: impl Iterator, + width: u16, + direction: Direction, + out: &mut BufWriter, + quoted: bool, + tab_size: usize, +) -> UResult<()> { + if width == 0 { + // If the width is 0 we print one single line + let mut printed_something = false; + for name in names { + if printed_something { + write!(out, " ")?; + } + printed_something = true; + write_os_str(out, &name)?; + } + if printed_something { + writeln!(out)?; + } + } else { + let names: Vec = { + let mut buf = Vec::new(); + names + .map(|n| { + // 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. + // + // Example: + // ``` + // $ ls + // 'a\nb' bar + // foo baz + // ^ ^ + // These spaces is added + // ``` + // FIXME: the Grid crate only supports &str, so can't display raw bytes + buf.clear(); + if quoted && !os_str_starts_with(&n, b"'") && !os_str_starts_with(&n, b"\"") { + buf.push(b' '); + } + buf.extend(n.as_encoded_bytes()); + String::from_utf8_lossy(&buf).into_owned() + }) + .collect() + }; + + // Since tab_size=0 means no \t, use Spaces separator for optimization. + let filling = match tab_size { + 0 => Filling::Spaces(DEFAULT_SEPARATOR_SIZE), + _ => Filling::Tabs { + spaces: DEFAULT_SEPARATOR_SIZE, + tab_size, + }, + }; + + let grid = Grid::new( + names, + GridOptions { + filling, + direction, + width: width as usize, + }, + ); + write!(out, "{grid}")?; + } + Ok(()) +} + +fn display_additional_leading_info( + item: &PathData, + padding: &PaddingCollection, + config: &Config, + out: &mut impl Write, +) -> UResult<()> { + #[cfg(unix)] + { + if config.inode { + let inode = padding.inode; + if let Some(md) = item.metadata() { + write!(out, "{:>inode$} ", display_inode(md))?; + } else { + write!(out, "{:>inode$} ", '?')?; + } + } + } + + if config.alloc_size { + let s: Cow<'_, str> = if let Some(md) = item.metadata() { + display_size(get_block_size(md, config), config).into() + } else { + "?".into() + }; + // extra space is insert to align the sizes, as needed for all formats, except for the comma format. + if config.format == Format::Commas { + out.write_all(s.as_bytes())?; + out.write_all(b" ")?; + } else { + let block_size = padding.block_size; + write!(out, "{s:>block_size$} ")?; + } + } + + Ok(()) +} + +// Currently getpwuid is `linux` target only. If it's broken state.out into +// a posix-compliant attribute this can be updated... +#[cfg(unix)] +fn display_uname<'a>( + metadata: &Metadata, + config: &Config, + uid_cache: &'a mut FxHashMap, +) -> &'a String { + let uid = metadata.uid(); + + uid_cache.entry(uid).or_insert_with(|| { + if config.long.numeric_uid_gid { + uid.to_string() + } else { + entries::uid2usr(uid).unwrap_or_else(|_| uid.to_string()) + } + }) +} + +#[cfg(unix)] +fn display_group<'a>( + metadata: &Metadata, + config: &Config, + gid_cache: &'a mut FxHashMap, +) -> &'a String { + let gid = metadata.gid(); + gid_cache.entry(gid).or_insert_with(|| { + if config.long.numeric_uid_gid { + gid.to_string() + } else { + entries::gid2grp(gid).unwrap_or_else(|_| gid.to_string()) + } + }) +} + +#[cfg(not(unix))] +fn display_uname(_metadata: &Metadata, _config: &Config, _uid_cache: &mut ()) -> &'static str { + "somebody" +} + +#[cfg(not(unix))] +fn display_group(_metadata: &Metadata, _config: &Config, _gid_cache: &mut ()) -> &'static str { + "somegroup" +} + +fn display_date( + metadata: &Metadata, + config: &Config, + recent_time_range: &RangeInclusive, + out: &mut Vec, +) -> UResult<()> { + let Some(time) = metadata_get_time(metadata, config.time) else { + out.extend(b"???"); + return Ok(()); + }; + + // Use "recent" format if the given date is considered recent (i.e., in the last 6 months), + // or if no "older" format is available. + let fmt = match &config.time_format_older { + Some(time_format_older) if !recent_time_range.contains(&time) => time_format_older, + _ => &config.time_format_recent, + }; + + format_system_time(out, time, fmt, FormatSystemTimeFallback::Integer) +} + +fn display_len_or_rdev(metadata: &Metadata, config: &Config) -> SizeOrDeviceId { + #[cfg(any( + target_os = "linux", + target_os = "macos", + target_os = "android", + target_os = "ios", + target_os = "freebsd", + target_os = "dragonfly", + target_os = "netbsd", + target_os = "openbsd", + target_os = "illumos", + target_os = "solaris" + ))] + { + let ft = metadata.file_type(); + if ft.is_char_device() || ft.is_block_device() { + // A type cast is needed here as the `dev_t` type varies across OSes. + let dev = metadata.rdev() as dev_t; + let major = major(dev); + let minor = minor(dev); + return SizeOrDeviceId::Device(major.to_string(), minor.to_string()); + } + } + let len_adjusted = { + let d = metadata.len() / config.file_size_block_size; + let r = metadata.len() % config.file_size_block_size; + if r == 0 { d } else { d + 1 } + }; + SizeOrDeviceId::Size(display_size(len_adjusted, config)) +} + +pub fn display_size(size: u64, config: &Config) -> String { + human_readable(size, config.size_format) +} + +/// Takes a [`PathData`] struct and returns a cell with a name ready for displaying. +/// +/// This function relies on the following parameters in the provided `&Config`: +/// * `config.quoting_style` to decide how we will escape `name` using [`locale_aware_escape_name`]. +/// * `config.inode` decides whether to display inode numbers beside names using [`display_inode`]. +/// * `config.color` decides whether it's going to color `name` using [`color_name`]. +/// * `config.indicator_style` to append specific characters to `name` using [`classify_file`]. +/// * `config.format` to display symlink targets if `Format::Long`. This function is also +/// responsible for coloring symlink target names if `config.color` is specified. +/// * `config.context` to prepend security context to `name` if compiled with `feat_selinux`. +/// * `config.hyperlink` decides whether to hyperlink the item +/// +/// Note that non-unicode sequences in symlink targets are dealt with using +/// [`std::path::Path::to_string_lossy`]. +#[allow(clippy::cognitive_complexity)] +fn display_item_name( + path: &PathData, + config: &Config, + prefix_context: Option, + more_info: Option, + mut style_manager: Option<&mut StyleManager>, + current_column: LazyCell usize>, +) -> DisplayItemName { + // This is our return value. We start by `&path.display_name` and modify it along the way. + let mut name = escape_name_with_locale(path.display_name(), config); + + let is_wrap = + |namelen: usize| config.width != 0 && *current_column + namelen > config.width.into(); + + if config.hyperlink { + name = create_hyperlink(&name, path); + } + + if let Some(style_manager) = style_manager.as_mut() { + let len = name.len(); + name = color_name(name, path, style_manager, None, is_wrap(len)); + } + + if config.format != Format::Long { + if let Some(info) = more_info { + let old_name = name; + name = info.into(); + name.push(&old_name); + } + } + + if config.indicator_style != IndicatorStyle::None { + let sym = classify_file(path); + + let char_opt = match config.indicator_style { + IndicatorStyle::Classify => sym, + IndicatorStyle::FileType => { + // Don't append an asterisk. + match sym { + Some('*') => None, + _ => sym, + } + } + IndicatorStyle::Slash => { + // Append only a slash. + match sym { + Some('/') => Some('/'), + _ => None, + } + } + IndicatorStyle::None => None, + }; + + if let Some(c) = char_opt { + let _ = name.write_char(c); + } + } + + let dired_name_len = if config.dired { name.len() } else { 0 }; + + if config.format == Format::Long + && path.file_type().is_some_and(FileType::is_symlink) + && !path.must_dereference + { + match path.path().read_link() { + Ok(target_path) => { + name.push(" -> "); + + // We might as well color the symlink output after the arrow. + // This makes extra system calls, but provides important information that + // people run `ls -l --color` are very interested in. + if let Some(style_manager) = &mut style_manager { + let escaped_target = escape_name_with_locale(target_path.as_os_str(), config); + // We get the absolute path to be able to construct PathData with valid Metadata. + // This is because relative symlinks will fail to get_metadata. + let absolute_target = if target_path.is_relative() { + match path.path().parent() { + Some(p) => &p.join(&target_path), + None => &target_path, + } + } else { + &target_path + }; + + match fs::canonicalize(absolute_target) { + Ok(resolved_target) => { + let target_data = PathData::new( + resolved_target.as_path().into(), + None, + target_path.file_name().map(Cow::Borrowed), + config, + false, + ); + + // Check if the target actually needs coloring + let md_option: Option = target_data + .metadata() + .cloned() + .or_else(|| target_data.p_buf.symlink_metadata().ok()); + let style = style_manager.colors.style_for_path_with_metadata( + &target_data.p_buf, + md_option.as_ref(), + ); + + if style.is_some() { + // Only apply coloring if there's actually a style + name.push(color_name( + escaped_target, + &target_data, + style_manager, + None, + is_wrap(name.len()), + )); + } else { + // For regular files with no coloring, just use plain text + name.push(escaped_target); + } + } + Err(_) => { + name.push( + style_manager.apply_missing_target_style( + escaped_target, + is_wrap(name.len()), + ), + ); + } + } + } else { + // If no coloring is required, we just use target as is. + // Apply the right quoting + name.push(escape_name_with_locale(target_path.as_os_str(), config)); + } + } + Err(err) => { + show!(LsError::IOErrorContext( + path.path().to_path_buf(), + err, + false + )); + } + } + } + + // Prepend the security context to the `name` and adjust `width` in order + // to get correct alignment from later calls to`display_grid()`. + if config.context { + if let Some(pad_count) = prefix_context { + let security_context: Cow<'_, str> = if matches!(config.format, Format::Commas) { + path.security_context(config).into() + } else { + pad_left(path.security_context(config), pad_count).into() + }; + + let old_name = name; + name = OsString::with_capacity(security_context.len() + 1 + old_name.len()); + name.push(security_context.as_ref()); + name.push(" "); + name.push(old_name); + } + } + + DisplayItemName { + displayed: name, + dired_name_len, + } +} + +/// This writes to the [`BufWriter`] `state.out` a single string of the output of `ls -l`. +/// +/// It writes the following keys, in order: +/// * `inode` ([`display_inode`], config-optional) +/// * `permissions` ([`display_permissions`]) +/// * `symlink_count` ([`display_symlink_count`]) +/// * `owner` ([`display_uname`], config-optional) +/// * `group` ([`display_group`], config-optional) +/// * `author` ([`display_uname`], config-optional) +/// * `size / rdev` ([`display_len_or_rdev`]) +/// * `system_time` ([`display_date`]) +/// * `item_name` ([`display_item_name`]) +/// +/// This function needs to display information in columns: +/// * permissions and `system_time` are already guaranteed to be pre-formatted in fixed length. +/// * `item_name` is the last column and is left-aligned. +/// * Everything else needs to be padded using [`pad_left`]. +/// +/// That's why we have the parameters: +/// ```txt +/// longest_link_count_len: usize, +/// longest_uname_len: usize, +/// longest_group_len: usize, +/// longest_context_len: usize, +/// longest_size_len: usize, +/// ``` +/// that decide the maximum possible character count of each field. +#[allow(clippy::write_literal)] +#[allow(clippy::cognitive_complexity)] +fn display_item_long( + item: &PathData, + padding: &PaddingCollection, + config: &Config, + state: &mut ListState, + dired: &mut DiredOutput, + quoted: bool, +) -> UResult<()> { + // apply normal color to non filename outputs + if let Some(style_manager) = &mut state.style_manager { + state + .display_buf + .extend(style_manager.apply_normal().as_bytes()); + } + if config.dired { + state.display_buf.extend(b" "); + } + if let Some(md) = item.metadata() { + #[cfg(any(not(unix), target_os = "android", target_os = "macos"))] + // TODO: See how Mac should work here + let is_acl_set = false; + #[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] + let is_acl_set = has_acl(item.path()); + state + .display_buf + .extend(display_permissions(md, true).as_bytes()); + if item.security_context(config).len() > 1 { + // GNU `ls` uses a "." character to indicate a file with a security context, + // but not other alternate access method. + state.display_buf.push(b'.'); + } else if is_acl_set { + state.display_buf.push(b'+'); + } else { + state.display_buf.push(b' '); + } + + state + .display_buf + .extend_pad_left(&display_symlink_count(md), padding.link_count); + + if config.long.owner { + state.display_buf.push(b' '); + state.display_buf.extend_pad_right( + display_uname(md, config, &mut state.uid_cache), + padding.uname, + ); + } + + if config.long.group { + state.display_buf.push(b' '); + state.display_buf.extend_pad_right( + display_group(md, config, &mut state.gid_cache), + padding.group, + ); + } + + if config.context { + state.display_buf.push(b' '); + state + .display_buf + .extend_pad_right(item.security_context(config), padding.context); + } + + // Author is only different from owner on GNU/Hurd, so we reuse + // the owner, since GNU/Hurd is not currently supported by Rust. + if config.long.author { + state.display_buf.push(b' '); + state.display_buf.extend_pad_right( + display_uname(md, config, &mut state.uid_cache), + padding.uname, + ); + } + + match display_len_or_rdev(md, config) { + SizeOrDeviceId::Size(size) => { + state.display_buf.push(b' '); + state.display_buf.extend_pad_left(&size, padding.size); + } + SizeOrDeviceId::Device(major, minor) => { + state.display_buf.push(b' '); + state.display_buf.extend_pad_left( + &major, + #[cfg(not(unix))] + 0usize, + #[cfg(unix)] + padding.major.max( + padding + .size + .saturating_sub(padding.minor.saturating_add(2usize)), + ), + ); + state.display_buf.extend(b", "); + state.display_buf.extend_pad_left( + &minor, + #[cfg(not(unix))] + 0usize, + #[cfg(unix)] + padding.minor, + ); + } + } + + state.display_buf.push(b' '); + display_date(md, config, &state.recent_time_range, &mut state.display_buf)?; + state.display_buf.push(b' '); + + let item_display = display_item_name( + item, + config, + None, + None, + state.style_manager.as_mut(), + LazyCell::new(|| ansi_width(&String::from_utf8_lossy(&state.display_buf))), + ); + + let needs_space = quoted && !os_str_starts_with(&item_display.displayed, b"'"); + + if config.dired { + let mut dired_name_len = item_display.dired_name_len; + if needs_space { + dired_name_len += 1; + } + let displayed_len = item_display.displayed.len() + usize::from(needs_space); + update_dired_for_item( + dired, + state.display_buf.len(), + displayed_len, + dired_name_len, + ); + } + + let item_name = item_display.displayed; + let displayed_item = if needs_space { + let mut ret = OsString::with_capacity(item_name.len() + 1); + let _ = ret.write_char(' '); + ret.push(&item_name); + ret + } else { + item_name + }; + + write_os_str(&mut state.display_buf, &displayed_item)?; + state.display_buf.push(config.line_ending as u8); + } else { + #[cfg(unix)] + let leading_char = { + if let Some(ft) = item.file_type() { + if ft.is_char_device() { + 'c' + } else if ft.is_block_device() { + 'b' + } else if ft.is_symlink() { + 'l' + } else if ft.is_dir() { + 'd' + } else { + '-' + } + } else if item.is_dangling_link() { + 'l' + } else { + '-' + } + }; + #[cfg(not(unix))] + let leading_char = { + if let Some(ft) = item.file_type() { + if ft.is_symlink() { + 'l' + } else if ft.is_dir() { + 'd' + } else { + '-' + } + } else if item.is_dangling_link() { + 'l' + } else { + '-' + } + }; + + state.display_buf.push(leading_char as u8); + state.display_buf.extend(b"?????????"); + if item.security_context(config).len() > 1 { + // GNU `ls` uses a "." character to indicate a file with a security context, + // but not other alternate access method. + state.display_buf.push(b'.'); + } + state.display_buf.push(b' '); + state.display_buf.extend_pad_left("?", padding.link_count); + + if config.long.owner { + state.display_buf.push(b' '); + state.display_buf.extend_pad_right("?", padding.uname); + } + + if config.long.group { + state.display_buf.push(b' '); + state.display_buf.extend_pad_right("?", padding.group); + } + + if config.context { + state.display_buf.push(b' '); + state + .display_buf + .extend_pad_right(item.security_context(config), padding.context); + } + + // Author is only different from owner on GNU/Hurd, so we reuse + // the owner, since GNU/Hurd is not currently supported by Rust. + if config.long.author { + state.display_buf.push(b' '); + state.display_buf.extend_pad_right("?", padding.uname); + } + + let displayed_item = display_item_name( + item, + config, + None, + None, + state.style_manager.as_mut(), + LazyCell::new(|| ansi_width(&String::from_utf8_lossy(&state.display_buf))), + ); + let date_len = 12; + + state.display_buf.push(b' '); + state.display_buf.extend_pad_left("?", padding.size); + state.display_buf.push(b' '); + state.display_buf.extend_pad_left("?", date_len); + state.display_buf.push(b' '); + + if config.dired { + update_dired_for_item( + dired, + state.display_buf.len(), + displayed_item.displayed.len(), + displayed_item.dired_name_len, + ); + } + let displayed_item = displayed_item.displayed; + write_os_str(&mut state.display_buf, &displayed_item)?; + state.display_buf.push(config.line_ending as u8); + } + state.out.write_all(&state.display_buf)?; + state.display_buf.clear(); + + Ok(()) +} + +fn classify_file(path: &PathData) -> Option { + let file_type = path.file_type()?; + + if file_type.is_dir() { + Some('/') + } else if file_type.is_symlink() { + Some('@') + } else { + #[cfg(unix)] + { + if file_type.is_socket() { + Some('=') + } else if file_type.is_fifo() { + Some('|') + // Safe unwrapping if the file was removed between listing and display + // See https://github.com/uutils/coreutils/issues/5371 + } else if path.is_executable_file() { + Some('*') + } else { + None + } + } + #[cfg(not(unix))] + None + } +} + +fn create_hyperlink(name: &OsStr, path: &PathData) -> OsString { + // The `hostname` crate does not support WASI (no OS-level hostname API), + // so we use an empty string for hyperlinks on WASI. + #[cfg(not(target_os = "wasi"))] + static HOSTNAME: LazyLock = LazyLock::new(|| hostname::get().unwrap_or_default()); + #[cfg(target_os = "wasi")] + static HOSTNAME: LazyLock = LazyLock::new(OsString::new); + + // OSC 8 hyperlink format: \x1b]8;;URL\x1b\\TEXT\x1b]8;;\x1b\\ + // \x1b = ESC, \x1b\\ = ESC backslash + // FIXME: switch to constants once OsStr::new() is const-stable and over our MSRV. + let osc_8_head = OsStr::new("\x1b]8;;file://"); + let osc_8_tail = OsStr::new("\x1b]8;;\x1b\\"); + let esc_bl = OsStr::new("\x1b\\"); + + let absolute_path = fs::canonicalize(path.path()).unwrap_or_default(); + let mut ret = OsString::with_capacity( + osc_8_head.len() + + osc_8_tail.len() + + HOSTNAME.len() + + esc_bl.len() + + absolute_path.as_os_str().len(), + ); + ret.push(osc_8_head); + ret.push(HOSTNAME.as_os_str()); + + // a set of safe ASCII bytes that don't need encoding + #[cfg(not(target_os = "windows"))] + let unencoded = |c| matches!(c, '_' | '-' | '.' | '~' | '/'); + #[cfg(target_os = "windows")] + let unencoded = |c| matches!(c, '_' | '-' | '.' | '~' | '/' | '\\' | ':'); + + for &b in absolute_path.as_os_str().as_encoded_bytes() { + if b.is_ascii_alphanumeric() || unencoded(b as char) { + let _ = ret.write_char(b as char); + } else { + let _ = write!(ret, "%{b:02x}"); + } + } + + ret.push(esc_bl); + ret.push(name); + ret.push(osc_8_tail); + + ret +} + +fn is_hidden(file_path: &DirEntry) -> bool { + #[cfg(windows)] + { + let metadata = file_path.metadata().unwrap(); + let attr = metadata.file_attributes(); + (attr & 0x2) > 0 + } + #[cfg(not(windows))] + { + file_path.file_name().as_encoded_bytes().starts_with(b".") + } +} + +fn update_dired_for_item( + dired: &mut DiredOutput, + output_display_len: usize, + displayed_len: usize, + dired_name_len: usize, +) { + let line_len = output_display_len + displayed_len + 1; // +1 for line ending + dired::calculate_and_update_positions(dired, output_display_len, dired_name_len, line_len); +} + +#[cfg(unix)] +fn display_symlink_count(metadata: &Metadata) -> String { + metadata.nlink().to_string() +} + +#[cfg(unix)] +fn display_inode(metadata: &Metadata) -> impl Display { + metadata.ino().to_string() +} + +#[cfg(unix)] +fn calculate_padding_collection( + items: &[PathData], + config: &Config, + state: &mut ListState, +) -> PaddingCollection { + let mut padding_collections = PaddingCollection { + inode: 1, + link_count: 1, + uname: 1, + group: 1, + context: 1, + size: 1, + major: 1, + minor: 1, + block_size: 1, + }; + + for item in items { + #[cfg(unix)] + if config.inode { + let inode_len = if let Some(md) = item.metadata() { + digits(md.ino()) + } else { + continue; + }; + padding_collections.inode = inode_len.max(padding_collections.inode); + } + + if config.alloc_size { + if let Some(md) = item.metadata() { + let block_size_len = display_size(get_block_size(md, config), config).len(); + padding_collections.block_size = block_size_len.max(padding_collections.block_size); + } + } + + if config.format == Format::Long { + let context_len = item.security_context(config).len(); + let (link_count_len, uname_len, group_len, size_len, major_len, minor_len) = + display_dir_entry_size(item, config, state); + padding_collections.link_count = link_count_len.max(padding_collections.link_count); + padding_collections.uname = uname_len.max(padding_collections.uname); + padding_collections.group = group_len.max(padding_collections.group); + if config.context { + padding_collections.context = context_len.max(padding_collections.context); + } + + // correctly align columns when some files have capabilities/ACLs and others do not + { + #[cfg(any(not(unix), target_os = "android", target_os = "macos"))] + // TODO: See how Mac should work here + let is_acl_set = false; + #[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] + let is_acl_set = has_acl(item.display_name()); + if context_len > 1 || is_acl_set { + padding_collections.link_count += 1; + } + } + + if items.len() == 1usize { + padding_collections.size = 0usize; + padding_collections.major = 0usize; + padding_collections.minor = 0usize; + } else { + padding_collections.major = major_len.max(padding_collections.major); + padding_collections.minor = minor_len.max(padding_collections.minor); + padding_collections.size = size_len + .max(padding_collections.size) + .max(padding_collections.major); + } + } + } + + padding_collections +} + +#[cfg(not(unix))] +fn display_symlink_count(_metadata: &Metadata) -> String { + // Currently not sure of how to get this on Windows, so I'm punting. + // Git Bash looks like it may do the same thing. + String::from("1") +} + +#[cfg(not(unix))] +fn calculate_padding_collection( + items: &[PathData], + config: &Config, + state: &mut ListState, +) -> PaddingCollection { + let mut padding_collections = PaddingCollection { + link_count: 1, + uname: 1, + group: 1, + context: 1, + size: 1, + block_size: 1, + }; + + for item in items { + if config.alloc_size { + if let Some(md) = item.metadata() { + let block_size_len = display_size(get_block_size(md, config), config).len(); + padding_collections.block_size = block_size_len.max(padding_collections.block_size); + } + } + + let context_len = item.security_context(config).len(); + let (link_count_len, uname_len, group_len, size_len, _major_len, _minor_len) = + display_dir_entry_size(item, config, state); + padding_collections.link_count = link_count_len.max(padding_collections.link_count); + padding_collections.uname = uname_len.max(padding_collections.uname); + padding_collections.group = group_len.max(padding_collections.group); + if config.context { + padding_collections.context = context_len.max(padding_collections.context); + } + padding_collections.size = size_len.max(padding_collections.size); + } + + 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/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 507689eee41..632d6ae591d 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -13,168 +13,51 @@ use std::borrow::Cow; use std::cell::RefCell; #[cfg(unix)] use std::os::unix::fs::{FileTypeExt, MetadataExt}; -#[cfg(windows)] -use std::os::windows::fs::MetadataExt; use std::{ - cell::{LazyCell, OnceCell}, + cell::OnceCell, cmp::Reverse, ffi::{OsStr, OsString}, - fmt::Write as _, fs::{self, DirEntry, FileType, Metadata, ReadDir}, - io::{BufWriter, ErrorKind, IsTerminal, Stdout, Write, stdout}, - iter, - num::IntErrorKind, + io::{BufWriter, ErrorKind, Stdout, Write, stdout}, ops::RangeInclusive, path::{Path, PathBuf}, time::{Duration, SystemTime, UNIX_EPOCH}, }; -use ansi_width::ansi_width; use clap::{ Arg, ArgAction, Command, builder::{NonEmptyStringValueParser, PossibleValue, ValueParser}, }; -use glob::{MatchOptions, Pattern}; -use lscolors::{Colorable, LsColors}; -use term_grid::{DEFAULT_SEPARATOR_SIZE, Direction, Filling, Grid, GridOptions, SPACES_IN_TAB}; +use lscolors::Colorable; use thiserror::Error; -#[cfg(unix)] -use uucore::entries; -#[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] -use uucore::fsxattr::has_acl; #[cfg(unix)] use uucore::libc::{S_IXGRP, S_IXOTH, S_IXUSR}; -#[cfg(any( - target_os = "linux", - target_os = "macos", - target_os = "android", - target_os = "ios", - target_os = "freebsd", - target_os = "dragonfly", - target_os = "netbsd", - target_os = "openbsd", - target_os = "illumos", - target_os = "solaris" -))] -use uucore::libc::{dev_t, major, minor}; use uucore::{ display::Quotable, error::{UError, UResult, set_exit_code}, - format::human::{SizeFormat, human_readable}, format_usage, fs::FileInformation, - fs::display_permissions, - fsext::{MetadataTimeField, metadata_get_time}, - line_ending::LineEnding, + fsext::metadata_get_time, os_str_as_bytes_lossy, - parser::parse_glob, - parser::parse_size::parse_size_non_zero_u64, parser::shortcut_value_parser::ShortcutValueParser, - quoting_style::{QuotingStyle, locale_aware_escape_dir_name, locale_aware_escape_name}, - show, show_error, show_warning, - time::{FormatSystemTimeFallback, format, format_system_time}, - translate, + show, translate, version_cmp::version_cmp, }; -mod dired; -use dired::{DiredOutput, is_dired_arg_present}; mod colors; -use crate::options::QUOTING_STYLE; -use colors::{LsColorsParseError, StyleManager, color_name, validate_ls_colors_env}; - -pub mod options { - pub mod format { - pub static ONE_LINE: &str = "1"; - pub static LONG: &str = "long"; - pub static COLUMNS: &str = "C"; - pub static ACROSS: &str = "x"; - pub static TAB_SIZE: &str = "tabsize"; - pub static COMMAS: &str = "m"; - pub static LONG_NO_OWNER: &str = "g"; - pub static LONG_NO_GROUP: &str = "o"; - pub static LONG_NUMERIC_UID_GID: &str = "numeric-uid-gid"; - } - - pub mod files { - pub static ALL: &str = "all"; - pub static ALMOST_ALL: &str = "almost-all"; - pub static UNSORTED_ALL: &str = "f"; - } - - pub mod sort { - pub static SIZE: &str = "S"; - pub static TIME: &str = "t"; - pub static NONE: &str = "U"; - pub static VERSION: &str = "v"; - pub static EXTENSION: &str = "X"; - } - - pub mod time { - pub static ACCESS: &str = "u"; - pub static CHANGE: &str = "c"; - } - - pub mod size { - pub static ALLOCATION_SIZE: &str = "size"; - pub static BLOCK_SIZE: &str = "block-size"; - pub static HUMAN_READABLE: &str = "human-readable"; - pub static SI: &str = "si"; - pub static KIBIBYTES: &str = "kibibytes"; - } - - pub mod quoting { - pub static ESCAPE: &str = "escape"; - pub static LITERAL: &str = "literal"; - pub static C: &str = "quote-name"; - } - - pub mod indicator_style { - pub static SLASH: &str = "p"; - pub static FILE_TYPE: &str = "file-type"; - pub static CLASSIFY: &str = "classify"; - } - - pub mod dereference { - pub static ALL: &str = "dereference"; - pub static ARGS: &str = "dereference-command-line"; - pub static DIR_ARGS: &str = "dereference-command-line-symlink-to-dir"; - } +mod config; +mod dired; +mod display; - pub static HELP: &str = "help"; - pub static QUOTING_STYLE: &str = "quoting-style"; - pub static HIDE_CONTROL_CHARS: &str = "hide-control-chars"; - pub static SHOW_CONTROL_CHARS: &str = "show-control-chars"; - pub static WIDTH: &str = "width"; - pub static AUTHOR: &str = "author"; - pub static NO_GROUP: &str = "no-group"; - pub static FORMAT: &str = "format"; - pub static SORT: &str = "sort"; - pub static TIME: &str = "time"; - pub static IGNORE_BACKUPS: &str = "ignore-backups"; - pub static DIRECTORY: &str = "directory"; - pub static INODE: &str = "inode"; - pub static REVERSE: &str = "reverse"; - pub static RECURSIVE: &str = "recursive"; - pub static COLOR: &str = "color"; - pub static PATHS: &str = "paths"; - pub static INDICATOR_STYLE: &str = "indicator-style"; - pub static TIME_STYLE: &str = "time-style"; - pub static FULL_TIME: &str = "full-time"; - pub static HIDE: &str = "hide"; - pub static IGNORE: &str = "ignore"; - pub static CONTEXT: &str = "context"; - pub static GROUP_DIRECTORIES_FIRST: &str = "group-directories-first"; - pub static ZERO: &str = "zero"; - pub static DIRED: &str = "dired"; - pub static HYPERLINK: &str = "hyperlink"; -} +pub use config::{Config, options}; +pub use display::Format; -const DEFAULT_TERM_WIDTH: u16 = 80; -const POSIXLY_CORRECT_BLOCK_SIZE: u64 = 512; -const DEFAULT_BLOCK_SIZE: u64 = 1024; -const DEFAULT_FILE_SIZE_BLOCK_SIZE: u64 = 1; +use colors::StyleManager; +use config::options::QUOTING_STYLE; +use config::{Dereference, Files, Sort}; +use dired::DiredOutput; +use display::{display_items, display_size, should_display, show_dir_name}; #[derive(Error, Debug)] enum LsError { @@ -185,6 +68,7 @@ enum LsError { IOError(#[from] std::io::Error), #[error("{}", match .1.kind() { + ErrorKind::NotADirectory => translate!("ls-error-not-directory", "path" => .0.quote()), ErrorKind::NotFound => translate!("ls-error-cannot-access-no-such-file", "path" => .0.quote()), ErrorKind::PermissionDenied => match .1.raw_os_error().unwrap_or(1) { 1 => translate!("ls-error-cannot-access-operation-not-permitted", "path" => .0.quote()), @@ -230,1037 +114,17 @@ impl UError for LsError { } } -#[derive(PartialEq, Eq, Debug)] -pub enum Format { - Columns, - Long, - OneLine, - Across, - Commas, -} - -#[derive(PartialEq, Eq)] -enum Sort { - None, - Name, - Size, - Time, - Version, - Extension, - Width, -} - -#[derive(PartialEq, Eq)] -enum Files { - All, - AlmostAll, - Normal, -} - -fn parse_time_style(options: &clap::ArgMatches) -> Result<(String, Option), LsError> { - // TODO: Using correct locale string is not implemented. - const LOCALE_FORMAT: (&str, Option<&str>) = ("%b %e %H:%M", Some("%b %e %Y")); - - // Convert time_styles references to owned String/option. - #[expect(clippy::unnecessary_wraps, reason = "internal result helper")] - fn ok((recent, older): (&str, Option<&str>)) -> Result<(String, Option), LsError> { - Ok((recent.to_string(), older.map(String::from))) - } - - if let Some(field) = options - .get_one::(options::TIME_STYLE) - .map(ToOwned::to_owned) - .or_else(|| std::env::var("TIME_STYLE").ok()) - { - //If both FULL_TIME and TIME_STYLE are present - //The one added last is dominant - if options.get_flag(options::FULL_TIME) - && options.indices_of(options::FULL_TIME).unwrap().next_back() - > options.indices_of(options::TIME_STYLE).unwrap().next_back() - { - ok((format::FULL_ISO, None)) - } else { - let field = if let Some(field) = field.strip_prefix("posix-") { - // See GNU documentation, set format to "locale" if LC_TIME="POSIX", - // else just strip the prefix and continue (even "posix+FORMAT" is - // supported). - // TODO: This needs to be moved to uucore and handled by icu? - if std::env::var_os("LC_TIME").as_deref() == Some(OsStr::new("POSIX")) - || std::env::var_os("LC_ALL").as_deref() == Some(OsStr::new("POSIX")) - { - return ok(LOCALE_FORMAT); - } - field - } else { - &field - }; - - match field { - "full-iso" => ok((format::FULL_ISO, None)), - "long-iso" => ok((format::LONG_ISO, None)), - // ISO older format needs extra padding. - "iso" => Ok(( - "%m-%d %H:%M".to_string(), - Some(format::ISO.to_string() + " "), - )), - "locale" => ok(LOCALE_FORMAT), - _ => match field.chars().next().unwrap() { - '+' => { - // recent/older formats are (optionally) separated by a newline - let mut it = field[1..].split('\n'); - let recent = it.next().unwrap_or_default(); - let older = it.next(); - match it.next() { - None => ok((recent, older)), - Some(_) => Err(LsError::TimeStyleParseError(String::from(field))), - } - } - _ => Err(LsError::TimeStyleParseError(String::from(field))), - }, - } - } - } else if options.get_flag(options::FULL_TIME) { - ok((format::FULL_ISO, None)) - } else { - ok(LOCALE_FORMAT) - } -} - -enum Dereference { - None, - DirArgs, - Args, - All, -} - -#[derive(PartialEq, Eq)] -enum IndicatorStyle { - None, - Slash, - FileType, - Classify, -} - -#[derive(Clone, Copy, PartialEq, Eq)] -enum LocaleQuoting { - Single, - Double, -} - -pub struct Config { - // Dir and vdir needs access to this field - pub format: Format, - files: Files, - sort: Sort, - recursive: bool, - reverse: bool, - dereference: Dereference, - ignore_patterns: Vec, - size_format: SizeFormat, - directory: bool, - time: MetadataTimeField, - #[cfg(unix)] - inode: bool, - color: Option, - long: LongFormat, - alloc_size: bool, - file_size_block_size: u64, - #[allow(dead_code)] - block_size: u64, // is never read on Windows - width: u16, - // Dir and vdir needs access to this field - pub quoting_style: QuotingStyle, - locale_quoting: Option, - indicator_style: IndicatorStyle, - time_format_recent: String, // Time format for recent dates - time_format_older: Option, // Time format for older dates (optional, if not present, time_format_recent is used) - context: bool, - #[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))] - selinux_supported: bool, - #[cfg(all(feature = "smack", target_os = "linux"))] - smack_supported: bool, - group_directories_first: bool, - line_ending: LineEnding, - dired: bool, - hyperlink: bool, - tab_size: usize, -} - -// Fields that can be removed or added to the long format -struct LongFormat { - author: bool, - group: bool, - owner: bool, - #[cfg(unix)] - numeric_uid_gid: bool, -} - -struct PaddingCollection { - #[cfg(unix)] - inode: usize, - link_count: usize, - uname: usize, - group: usize, - context: usize, - size: usize, - #[cfg(unix)] - major: usize, - #[cfg(unix)] - minor: usize, - block_size: usize, -} - -struct DisplayItemName { - displayed: OsString, - dired_name_len: usize, -} - -/// Extracts the format to display the information based on the options provided. -/// -/// # Returns -/// -/// A tuple containing the Format variant and an Option containing a &'static str -/// which corresponds to the option used to define the format. -fn extract_format(options: &clap::ArgMatches) -> (Format, Option<&'static str>) { - if let Some(format_) = options.get_one::(options::FORMAT) { - ( - match format_.as_str() { - "long" | "verbose" => Format::Long, - "single-column" => Format::OneLine, - "columns" | "vertical" => Format::Columns, - "across" | "horizontal" => Format::Across, - "commas" => Format::Commas, - // below should never happen as clap already restricts the values. - _ => unreachable!("Invalid field for --format"), - }, - Some(options::FORMAT), - ) - } else if options.get_flag(options::format::LONG) { - (Format::Long, Some(options::format::LONG)) - } else if options.get_flag(options::format::ACROSS) { - (Format::Across, Some(options::format::ACROSS)) - } else if options.get_flag(options::format::COMMAS) { - (Format::Commas, Some(options::format::COMMAS)) - } else if options.get_flag(options::format::COLUMNS) { - (Format::Columns, Some(options::format::COLUMNS)) - } else if stdout().is_terminal() { - (Format::Columns, None) - } else { - (Format::OneLine, None) - } -} - -/// Extracts the type of files to display -/// -/// # Returns -/// -/// A Files variant representing the type of files to display. -fn extract_files(options: &clap::ArgMatches) -> Files { - let get_last_index = |flag: &str| -> usize { - if options.value_source(flag) == Some(clap::parser::ValueSource::CommandLine) { - options.index_of(flag).unwrap_or(0) - } else { - 0 - } - }; - - let all_index = get_last_index(options::files::ALL); - let almost_all_index = get_last_index(options::files::ALMOST_ALL); - let unsorted_all_index = get_last_index(options::files::UNSORTED_ALL); - - let max_index = all_index.max(almost_all_index).max(unsorted_all_index); - - if max_index == 0 { - Files::Normal - } else if max_index == almost_all_index { - Files::AlmostAll - } else { - // Either -a or -f wins, both show all files - Files::All - } -} - -/// Extracts the sorting method to use based on the options provided. -/// -/// # Returns -/// -/// A Sort variant representing the sorting method to use. -fn extract_sort(options: &clap::ArgMatches) -> Sort { - let get_last_index = |flag: &str| -> usize { - if options.value_source(flag) == Some(clap::parser::ValueSource::CommandLine) { - options.index_of(flag).unwrap_or(0) - } else { - 0 - } - }; - - let sort_index = options - .get_one::(options::SORT) - .and_then(|_| options.indices_of(options::SORT)) - .map_or(0, |mut indices| indices.next_back().unwrap_or(0)); - let time_index = get_last_index(options::sort::TIME); - let size_index = get_last_index(options::sort::SIZE); - let none_index = get_last_index(options::sort::NONE); - let version_index = get_last_index(options::sort::VERSION); - let extension_index = get_last_index(options::sort::EXTENSION); - let unsorted_all_index = get_last_index(options::files::UNSORTED_ALL); - - let max_sort_index = sort_index - .max(time_index) - .max(size_index) - .max(none_index) - .max(version_index) - .max(extension_index) - .max(unsorted_all_index); - - match max_sort_index { - 0 => { - // No sort flags specified, use default behavior - if !options.get_flag(options::format::LONG) - && (options.get_flag(options::time::ACCESS) - || options.get_flag(options::time::CHANGE) - || options.get_one::(options::TIME).is_some()) - { - Sort::Time - } else { - Sort::Name - } - } - idx if idx == unsorted_all_index || idx == none_index => Sort::None, - idx if idx == sort_index => { - if let Some(field) = options.get_one::(options::SORT) { - match field.as_str() { - "none" => Sort::None, - "name" => Sort::Name, - "time" => Sort::Time, - "size" => Sort::Size, - "version" => Sort::Version, - "extension" => Sort::Extension, - "width" => Sort::Width, - _ => unreachable!("Invalid field for --sort"), - } - } else { - Sort::Name - } - } - idx if idx == time_index => Sort::Time, - idx if idx == size_index => Sort::Size, - idx if idx == version_index => Sort::Version, - idx if idx == extension_index => Sort::Extension, - _ => Sort::Name, - } -} - -/// Extracts the time to use based on the options provided. -/// -/// # Returns -/// -/// A `MetadataTimeField` variant representing the time to use. -fn extract_time(options: &clap::ArgMatches) -> MetadataTimeField { - if let Some(field) = options.get_one::(options::TIME) { - field.as_str().into() - } else if options.get_flag(options::time::ACCESS) { - MetadataTimeField::Access - } else if options.get_flag(options::time::CHANGE) { - MetadataTimeField::Change - } else { - MetadataTimeField::Modification - } -} - -/// Some env variables can be passed -/// For now, we are only verifying if empty or not and known for `TERM` -fn is_color_compatible_term() -> bool { - let is_term_set = std::env::var("TERM").is_ok(); - let is_colorterm_set = std::env::var("COLORTERM").is_ok(); - - let term = std::env::var("TERM").unwrap_or_default(); - let colorterm = std::env::var("COLORTERM").unwrap_or_default(); - - // Search function in the TERM struct to manage the wildcards - let term_matches = |term: &str| -> bool { - uucore::colors::TERMS.iter().any(|&pattern| { - term == pattern - || (pattern.ends_with('*') && term.starts_with(&pattern[..pattern.len() - 1])) - }) - }; - - if is_term_set && term.is_empty() && is_colorterm_set && colorterm.is_empty() { - return false; - } - - if !term.is_empty() && !term_matches(&term) { - return false; - } - true -} - -/// Extracts the color option to use based on the options provided. -/// -/// # Returns -/// -/// A boolean representing whether or not to use color. -fn extract_color(options: &clap::ArgMatches) -> bool { - if !is_color_compatible_term() { - return false; - } - - let get_last_index = |flag: &str| -> usize { - if options.value_source(flag) == Some(clap::parser::ValueSource::CommandLine) { - options.index_of(flag).unwrap_or(0) - } else { - 0 - } - }; - - let color_index = options - .get_one::(options::COLOR) - .and_then(|_| options.indices_of(options::COLOR)) - .map_or(0, |mut indices| indices.next_back().unwrap_or(0)); - let unsorted_all_index = get_last_index(options::files::UNSORTED_ALL); - - let color_enabled = match options.get_one::(options::COLOR) { - None => options.contains_id(options::COLOR), - Some(val) => match val.as_str() { - "" | "always" | "yes" | "force" => true, - "auto" | "tty" | "if-tty" => stdout().is_terminal(), - /* "never" | "no" | "none" | */ _ => false, - }, - }; - - // If --color was explicitly specified, always honor it regardless of -f - // Otherwise, if -f is present without explicit color, disable color - if color_index > 0 { - // Color was explicitly specified - color_enabled - } else if unsorted_all_index > 0 { - // -f present without explicit color, disable implicit color - false - } else { - color_enabled - } -} - -/// Extracts the hyperlink option to use based on the options provided. -/// -/// # Returns -/// -/// A boolean representing whether to hyperlink files. -fn extract_hyperlink(options: &clap::ArgMatches) -> bool { - let hyperlink = options - .get_one::(options::HYPERLINK) - .unwrap() - .as_str(); - - match hyperlink { - "always" | "yes" | "force" => true, - "auto" | "tty" | "if-tty" => stdout().is_terminal(), - "never" | "no" | "none" => false, - _ => unreachable!("should be handled by clap"), - } -} - -/// Match the argument given to --quoting-style or the [`QUOTING_STYLE`] env variable. -/// -/// # Arguments -/// -/// * `style`: the actual argument string -/// * `show_control` - A boolean value representing whether to show control characters. -/// -/// # Returns -/// -/// * An option with None if the style string is invalid, or a `QuotingStyle` wrapped in `Some`. -struct QuotingStyleSpec { - style: QuotingStyle, - fixed_control: bool, - locale: Option, -} - -impl QuotingStyleSpec { - fn new(style: QuotingStyle) -> Self { - Self { - style, - fixed_control: false, - locale: None, - } - } - - fn with_locale(style: QuotingStyle, locale: LocaleQuoting) -> Self { - Self { - style, - fixed_control: true, - locale: Some(locale), - } - } -} - -fn match_quoting_style_name( - style: &str, - show_control: bool, -) -> Option<(QuotingStyle, Option)> { - let spec = match style { - "literal" => QuotingStyleSpec::new(QuotingStyle::Literal { - show_control: false, - }), - "shell" => QuotingStyleSpec::new(QuotingStyle::SHELL), - "shell-always" => QuotingStyleSpec::new(QuotingStyle::SHELL_QUOTE), - "shell-escape" => QuotingStyleSpec::new(QuotingStyle::SHELL_ESCAPE), - "shell-escape-always" => QuotingStyleSpec::new(QuotingStyle::SHELL_ESCAPE_QUOTE), - "c" => QuotingStyleSpec::new(QuotingStyle::C_DOUBLE), - "escape" => QuotingStyleSpec::new(QuotingStyle::C_NO_QUOTES), - "locale" => QuotingStyleSpec { - style: QuotingStyle::Literal { - show_control: false, - }, - fixed_control: true, - locale: Some(LocaleQuoting::Single), - }, - "clocale" => QuotingStyleSpec::with_locale(QuotingStyle::C_DOUBLE, LocaleQuoting::Double), - _ => return None, - }; - - let style = if spec.fixed_control { - spec.style - } else { - spec.style.show_control(show_control) - }; - - Some((style, spec.locale)) -} - -/// Extracts the quoting style to use based on the options provided. -/// If no options are given, it looks if a default quoting style is provided -/// through the [`QUOTING_STYLE`] environment variable. -/// -/// # Arguments -/// -/// * `options` - A reference to a [`clap::ArgMatches`] object containing command line arguments. -/// * `show_control` - A boolean value representing whether or not to show control characters. -/// -/// # Returns -/// -/// A [`QuotingStyle`] variant representing the quoting style to use. -fn extract_quoting_style( - options: &clap::ArgMatches, - show_control: bool, -) -> (QuotingStyle, Option) { - let opt_quoting_style = options.get_one::(QUOTING_STYLE); - - if let Some(style) = opt_quoting_style { - match match_quoting_style_name(style, show_control) { - Some(pair) => pair, - None => unreachable!("Should have been caught by Clap"), - } - } else if options.get_flag(options::quoting::LITERAL) { - (QuotingStyle::Literal { show_control }, None) - } else if options.get_flag(options::quoting::ESCAPE) { - (QuotingStyle::C_NO_QUOTES, None) - } else if options.get_flag(options::quoting::C) { - (QuotingStyle::C_DOUBLE, None) - } else if options.get_flag(options::DIRED) { - (QuotingStyle::Literal { show_control }, None) - } else { - // If set, the QUOTING_STYLE environment variable specifies a default style. - if let Ok(style) = std::env::var("QUOTING_STYLE") { - match match_quoting_style_name(style.as_str(), show_control) { - Some(pair) => return pair, - None => eprintln!( - "{}", - translate!("ls-invalid-quoting-style", "program" => std::env::args().next().unwrap_or_else(|| "ls".to_string()), "style" => style.clone()) - ), - } - } - - // By default, `ls` uses Shell escape quoting style when writing to a terminal file - // descriptor and Literal otherwise. - if stdout().is_terminal() { - (QuotingStyle::SHELL_ESCAPE.show_control(show_control), None) - } else { - (QuotingStyle::Literal { show_control }, None) - } - } -} - -/// Extracts the indicator style to use based on the options provided. -/// -/// # Returns -/// -/// An [`IndicatorStyle`] variant representing the indicator style to use. -fn extract_indicator_style(options: &clap::ArgMatches) -> IndicatorStyle { - if let Some(field) = options.get_one::(options::INDICATOR_STYLE) { - match field.as_str() { - "none" => IndicatorStyle::None, - "file-type" => IndicatorStyle::FileType, - "classify" => IndicatorStyle::Classify, - "slash" => IndicatorStyle::Slash, - &_ => IndicatorStyle::None, - } - } else if let Some(field) = options.get_one::(options::indicator_style::CLASSIFY) { - match field.as_str() { - "never" | "no" | "none" => IndicatorStyle::None, - "always" | "yes" | "force" => IndicatorStyle::Classify, - "auto" | "tty" | "if-tty" => { - if stdout().is_terminal() { - IndicatorStyle::Classify - } else { - IndicatorStyle::None - } - } - &_ => IndicatorStyle::None, - } - } else if options.get_flag(options::indicator_style::SLASH) { - IndicatorStyle::Slash - } else if options.get_flag(options::indicator_style::FILE_TYPE) { - IndicatorStyle::FileType - } else { - IndicatorStyle::None - } -} - -/// Parses the width value from either the command line arguments or the environment variables. -fn parse_width(width_match: Option<&String>) -> Result { - let parse_width_from_args = |s: &str| -> Result { - let radix = if s.starts_with('0') && s.len() > 1 { - 8 - } else { - 10 - }; - match u16::from_str_radix(s, radix) { - Ok(x) => Ok(x), - Err(e) => match e.kind() { - IntErrorKind::PosOverflow => Ok(u16::MAX), - _ => Err(LsError::InvalidLineWidth(s.into())), - }, - } - }; - - let parse_width_from_env = |columns: OsString| { - if let Some(columns) = columns.to_str().and_then(|s| s.parse().ok()) { - columns - } else { - show_error!( - "{}", - translate!("ls-invalid-columns-width", "width" => columns.quote()) - ); - DEFAULT_TERM_WIDTH - } - }; +#[uucore::main] +pub fn uumain(args: impl uucore::Args) -> UResult<()> { + let matches = uucore::clap_localization::handle_clap_result_with_exit_code(uu_app(), args, 2)?; - let calculate_term_size = || match terminal_size::terminal_size() { - Some((width, _)) => width.0, - None => DEFAULT_TERM_WIDTH, - }; + let config = Config::from(&matches)?; - let ret = match width_match { - Some(x) => parse_width_from_args(x)?, - None => match std::env::var_os("COLUMNS") { - Some(columns) => parse_width_from_env(columns), - None => calculate_term_size(), - }, - }; + let locs = matches + .get_many::(options::PATHS) + .map_or_else(|| vec![Path::new(".")], |v| v.map(Path::new).collect()); - Ok(ret) -} - -impl Config { - #[allow(clippy::cognitive_complexity)] - pub fn from(options: &clap::ArgMatches) -> UResult { - let context = options.get_flag(options::CONTEXT); - let (mut format, opt) = extract_format(options); - let files = extract_files(options); - - // The -o, -n and -g options are tricky. They cannot override with each - // other because it's possible to combine them. For example, the option - // -og should hide both owner and group. Furthermore, they are not - // reset if -l or --format=long is used. So these should just show the - // group: -gl or "-g --format=long". Finally, they are also not reset - // when switching to a different format option in-between like this: - // -ogCl or "-og --format=vertical --format=long". - // - // -1 has a similar issue: it does nothing if the format is long. This - // actually makes it distinct from the --format=singe-column option, - // which always applies. - // - // The idea here is to not let these options override with the other - // options, but manually whether they have an index that's greater than - // the other format options. If so, we set the appropriate format. - if format != Format::Long { - let idx = opt - .and_then(|opt| options.indices_of(opt).map(|x| x.max().unwrap())) - .unwrap_or(0); - if [ - options::format::LONG_NO_OWNER, - options::format::LONG_NO_GROUP, - options::format::LONG_NUMERIC_UID_GID, - options::FULL_TIME, - ] - .iter() - .filter_map(|opt| { - if options.value_source(opt) == Some(clap::parser::ValueSource::CommandLine) { - options.indices_of(opt) - } else { - None - } - }) - .flatten() - .any(|i| i >= idx) - { - format = Format::Long; - } else if let Some(mut indices) = options.indices_of(options::format::ONE_LINE) { - if options.value_source(options::format::ONE_LINE) - == Some(clap::parser::ValueSource::CommandLine) - && indices.any(|i| i > idx) - { - format = Format::OneLine; - } - } - } - - let sort = extract_sort(options); - let time = extract_time(options); - let mut needs_color = extract_color(options); - let hyperlink = extract_hyperlink(options); - - let opt_block_size = options.get_one::(options::size::BLOCK_SIZE); - let opt_si = opt_block_size.is_some() - && options - .get_one::(options::size::BLOCK_SIZE) - .unwrap() - .eq("si") - || options.get_flag(options::size::SI); - let opt_hr = (opt_block_size.is_some() - && options - .get_one::(options::size::BLOCK_SIZE) - .unwrap() - .eq("human-readable")) - || options.get_flag(options::size::HUMAN_READABLE); - let opt_kb = options.get_flag(options::size::KIBIBYTES); - - let size_format = if opt_si { - SizeFormat::Decimal - } else if opt_hr { - SizeFormat::Binary - } else { - SizeFormat::Bytes - }; - - let env_var_blocksize = std::env::var_os("BLOCKSIZE"); - let env_var_block_size = std::env::var_os("BLOCK_SIZE"); - let env_var_ls_block_size = std::env::var_os("LS_BLOCK_SIZE"); - let env_var_posixly_correct = std::env::var_os("POSIXLY_CORRECT"); - let mut is_env_var_blocksize = false; - - let raw_block_size = if let Some(opt_block_size) = opt_block_size { - OsString::from(opt_block_size) - } else if let Some(env_var_ls_block_size) = env_var_ls_block_size { - env_var_ls_block_size - } else if let Some(env_var_block_size) = env_var_block_size { - env_var_block_size - } else if let Some(env_var_blocksize) = env_var_blocksize { - is_env_var_blocksize = true; - env_var_blocksize - } else { - OsString::from("") - }; - - let (file_size_block_size, block_size) = if !opt_si && !opt_hr && !raw_block_size.is_empty() - { - if let Ok(size) = parse_size_non_zero_u64(&raw_block_size.to_string_lossy()) { - match (is_env_var_blocksize, opt_kb) { - (true, true) => (DEFAULT_FILE_SIZE_BLOCK_SIZE, DEFAULT_BLOCK_SIZE), - (true, false) => (DEFAULT_FILE_SIZE_BLOCK_SIZE, size), - (false, true) => { - // --block-size overrides -k - if opt_block_size.is_some() { - (size, size) - } else { - (size, DEFAULT_BLOCK_SIZE) - } - } - (false, false) => (size, size), - } - } else { - // only fail if invalid block size was specified with --block-size, - // ignore invalid block size from env vars - if let Some(invalid_block_size) = opt_block_size { - return Err(Box::new(LsError::BlockSizeParseError( - invalid_block_size.clone(), - ))); - } - if is_env_var_blocksize { - (DEFAULT_FILE_SIZE_BLOCK_SIZE, DEFAULT_BLOCK_SIZE) - } else { - (DEFAULT_BLOCK_SIZE, DEFAULT_BLOCK_SIZE) - } - } - } else if env_var_posixly_correct.is_some() { - if opt_kb { - (DEFAULT_FILE_SIZE_BLOCK_SIZE, DEFAULT_BLOCK_SIZE) - } else { - (DEFAULT_FILE_SIZE_BLOCK_SIZE, POSIXLY_CORRECT_BLOCK_SIZE) - } - } else if opt_si { - (DEFAULT_FILE_SIZE_BLOCK_SIZE, 1000) - } else { - (DEFAULT_FILE_SIZE_BLOCK_SIZE, DEFAULT_BLOCK_SIZE) - }; - - let long = { - let author = options.get_flag(options::AUTHOR); - let group = !options.get_flag(options::NO_GROUP) - && !options.get_flag(options::format::LONG_NO_GROUP); - let owner = !options.get_flag(options::format::LONG_NO_OWNER); - #[cfg(unix)] - let numeric_uid_gid = options.get_flag(options::format::LONG_NUMERIC_UID_GID); - LongFormat { - author, - group, - owner, - #[cfg(unix)] - numeric_uid_gid, - } - }; - let width = parse_width(options.get_one::(options::WIDTH))?; - - #[allow(clippy::needless_bool)] - let mut show_control = if options.get_flag(options::HIDE_CONTROL_CHARS) { - false - } else if options.get_flag(options::SHOW_CONTROL_CHARS) { - true - } else { - !stdout().is_terminal() - }; - - let (mut quoting_style, mut locale_quoting) = extract_quoting_style(options, show_control); - let indicator_style = extract_indicator_style(options); - // Only parse the value to "--time-style" if it will become relevant. - let dired = options.get_flag(options::DIRED); - let (time_format_recent, time_format_older) = if format == Format::Long || dired { - parse_time_style(options)? - } else { - Default::default() - }; - - let mut ignore_patterns: Vec = Vec::new(); - - if options.get_flag(options::IGNORE_BACKUPS) { - ignore_patterns.push(Pattern::new("*~").unwrap()); - ignore_patterns.push(Pattern::new(".*~").unwrap()); - } - - for pattern in options - .get_many::(options::IGNORE) - .into_iter() - .flatten() - { - if let Ok(p) = parse_glob::from_str(pattern) { - ignore_patterns.push(p); - } else { - show_warning!( - "{}", - translate!("ls-invalid-ignore-pattern", "pattern" => pattern.quote()) - ); - } - } - - if files == Files::Normal { - for pattern in options - .get_many::(options::HIDE) - .into_iter() - .flatten() - { - if let Ok(p) = parse_glob::from_str(pattern) { - ignore_patterns.push(p); - } else { - show_warning!( - "{}", - translate!("ls-invalid-hide-pattern", "pattern" => pattern.quote()) - ); - } - } - } - - // According to ls info page, `--zero` implies the following flags: - // - `--show-control-chars` - // - `--format=single-column` - // - `--color=none` - // - `--quoting-style=literal` - // Current GNU ls implementation allows `--zero` Behavior to be - // overridden by later flags. - let zero_formats_opts = [ - options::format::ACROSS, - options::format::COLUMNS, - options::format::COMMAS, - options::format::LONG, - options::format::LONG_NO_GROUP, - options::format::LONG_NO_OWNER, - options::format::LONG_NUMERIC_UID_GID, - options::format::ONE_LINE, - options::FORMAT, - ]; - let zero_colors_opts = [options::COLOR]; - let zero_show_control_opts = [options::HIDE_CONTROL_CHARS, options::SHOW_CONTROL_CHARS]; - let zero_quoting_style_opts = [ - QUOTING_STYLE, - options::quoting::C, - options::quoting::ESCAPE, - options::quoting::LITERAL, - ]; - let get_last = |flag: &str| -> usize { - if options.value_source(flag) == Some(clap::parser::ValueSource::CommandLine) { - options.index_of(flag).unwrap_or(0) - } else { - 0 - } - }; - if get_last(options::ZERO) - > zero_formats_opts - .into_iter() - .map(get_last) - .max() - .unwrap_or(0) - { - format = if format == Format::Long { - format - } else { - Format::OneLine - }; - } - if get_last(options::ZERO) - > zero_colors_opts - .into_iter() - .map(get_last) - .max() - .unwrap_or(0) - { - needs_color = false; - } - if get_last(options::ZERO) - > zero_show_control_opts - .into_iter() - .map(get_last) - .max() - .unwrap_or(0) - { - show_control = true; - } - if get_last(options::ZERO) - > zero_quoting_style_opts - .into_iter() - .map(get_last) - .max() - .unwrap_or(0) - { - quoting_style = QuotingStyle::Literal { show_control }; - locale_quoting = None; - } - - if needs_color { - if let Err(err) = validate_ls_colors_env() { - if let LsColorsParseError::UnrecognizedPrefix(prefix) = &err { - show_warning!( - "{}", - translate!( - "ls-warning-unrecognized-ls-colors-prefix", - "prefix" => prefix.quote() - ) - ); - } - show_warning!("{}", translate!("ls-warning-unparsable-ls-colors")); - needs_color = false; - } - } - - let color = if needs_color { - Some(LsColors::from_env().unwrap_or_default()) - } else { - None - }; - - if dired || is_dired_arg_present() { - // --dired implies --format=long - // if we have --dired --hyperlink, we don't show dired but we still want to see the - // long format - format = Format::Long; - } - if dired && options.get_flag(options::ZERO) { - return Err(Box::new(LsError::DiredAndZeroAreIncompatible)); - } - - let dereference = if options.get_flag(options::dereference::ALL) { - Dereference::All - } else if options.get_flag(options::dereference::ARGS) { - Dereference::Args - } else if options.get_flag(options::dereference::DIR_ARGS) { - Dereference::DirArgs - } else if options.get_flag(options::DIRECTORY) - || indicator_style == IndicatorStyle::Classify - || format == Format::Long - { - Dereference::None - } else { - Dereference::DirArgs - }; - - let tab_size = if needs_color { - Some(0) - } else { - options - .get_one::(options::format::TAB_SIZE) - .and_then(|size| size.parse::().ok()) - .or_else(|| std::env::var("TABSIZE").ok().and_then(|s| s.parse().ok())) - } - .unwrap_or(SPACES_IN_TAB); - - Ok(Self { - format, - files, - sort, - recursive: options.get_flag(options::RECURSIVE), - reverse: options.get_flag(options::REVERSE), - dereference, - ignore_patterns, - size_format, - directory: options.get_flag(options::DIRECTORY), - time, - color, - #[cfg(unix)] - inode: options.get_flag(options::INODE), - long, - alloc_size: options.get_flag(options::size::ALLOCATION_SIZE), - file_size_block_size, - block_size, - width, - quoting_style, - locale_quoting, - indicator_style, - time_format_recent, - time_format_older, - context, - #[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))] - selinux_supported: uucore::selinux::is_selinux_enabled(), - #[cfg(all(feature = "smack", target_os = "linux"))] - smack_supported: uucore::smack::is_smack_enabled(), - group_directories_first: options.get_flag(options::GROUP_DIRECTORIES_FIRST), - line_ending: LineEnding::from_zero_flag(options.get_flag(options::ZERO)), - dired, - hyperlink, - tab_size, - }) - } -} - -#[uucore::main] -pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uucore::clap_localization::handle_clap_result_with_exit_code(uu_app(), args, 2)?; - - let config = Config::from(&matches)?; - - let locs = matches - .get_many::(options::PATHS) - .map_or_else(|| vec![Path::new(".")], |v| v.map(Path::new).collect()); - - list(locs, &config) + list(locs, &config) } pub fn uu_app() -> Command { @@ -1921,45 +785,56 @@ pub fn uu_app() -> Command { .after_help(translate!("ls-after-help")) } +/// Represents the possible values of [`PathData::display_name`]. The reason this is a +/// separate enum is to avoid a self-referential struct, as it is moved in hot loops. +#[derive(Debug)] +enum PathDataDisplayName<'a> { + SelfReferential, + Custom(Cow<'a, OsStr>), +} + /// Represents a Path along with it's associated data. /// Any data that will be reused several times makes sense to be added to this structure. /// Caching data here helps eliminate redundant syscalls to fetch same information. #[derive(Debug)] -struct PathData { +struct PathData<'a> { // Result got from symlink_metadata() or metadata() based on config md: OnceCell>, ft: OnceCell>, // can be used to avoid reading the filetype. Can be also called d_type: // https://www.gnu.org/software/libc/manual/html_node/Directory-Entries.html - de: RefCell>>, + de: RefCell>, security_context: OnceCell>, // Name of the file - will be empty for . or .. - display_name: OsString, + display_name: PathDataDisplayName<'a>, // PathBuf that all above data corresponds to - p_buf: PathBuf, + p_buf: Cow<'a, Path>, must_dereference: bool, command_line: bool, } -impl PathData { +impl<'a> PathData<'a> { fn new( - p_buf: PathBuf, + p_buf: Cow<'a, Path>, dir_entry: Option, - file_name: Option, + file_name: Option>, config: &Config, command_line: bool, ) -> Self { // We cannot use `Path::ends_with` or `Path::Components`, because they remove occurrences of '.' // For '..', the filename is None let display_name = if let Some(name) = file_name { - name + PathDataDisplayName::Custom(name) } else if command_line { - p_buf.as_os_str().to_os_string() + PathDataDisplayName::SelfReferential } else { - dir_entry - .as_ref() - .map(DirEntry::file_name) - .unwrap_or_default() + PathDataDisplayName::Custom( + dir_entry + .as_ref() + .map(DirEntry::file_name) + .unwrap_or_default() + .into(), + ) }; let must_dereference = match &config.dereference { @@ -1985,11 +860,11 @@ impl PathData { let md: OnceCell> = OnceCell::new(); let security_context: OnceCell> = OnceCell::new(); - let de: RefCell>> = if let Some(de) = dir_entry { + let de: RefCell> = if let Some(de) = dir_entry { if must_dereference { if let Ok(md_pb) = p_buf.metadata() { - md.get_or_init(|| Some(md_pb.clone())); ft.get_or_init(|| Some(md_pb.file_type())); + md.get_or_init(|| Some(md_pb)); } } @@ -1997,7 +872,7 @@ impl PathData { ft.get_or_init(|| Some(ft_de)); } - RefCell::new(Some(de.into())) + RefCell::new(Some(de)) } else { RefCell::new(None) }; @@ -2018,7 +893,7 @@ impl PathData { self.md .get_or_init(|| { if !self.must_dereference { - if let Some(dir_entry) = RefCell::take(&self.de).as_deref() { + if let Some(dir_entry) = RefCell::take(&self.de) { return dir_entry.metadata().ok(); } } @@ -2078,11 +953,14 @@ impl PathData { } fn display_name(&self) -> &OsStr { - &self.display_name + match self.display_name { + PathDataDisplayName::SelfReferential => self.p_buf.as_os_str(), + PathDataDisplayName::Custom(ref cow) => cow, + } } } -impl Colorable for PathData { +impl Colorable for PathData<'_> { fn file_name(&self) -> OsString { self.display_name().to_os_string() } @@ -2097,101 +975,10 @@ impl Colorable for PathData { } } -/// Show the directory name in the case where several arguments are given to ls -/// or the recursive flag is passed. -/// -/// ```no-exec -/// $ ls -R -/// .: <- This is printed by this function -/// dir1 file1 file2 -/// -/// dir1: <- This as well -/// file11 -/// ``` -fn show_dir_name( - path_data: &PathData, - out: &mut BufWriter, - config: &Config, -) -> std::io::Result<()> { - let escaped_name = escape_dir_name_with_locale(path_data.path().as_os_str(), config); - - let name = if config.hyperlink && !config.dired { - create_hyperlink(&escaped_name, path_data) - } else { - escaped_name - }; - - write_os_str(out, &name)?; - write!(out, ":") -} - -fn escape_with_locale(name: &OsStr, config: &Config, fallback: F) -> OsString -where - F: FnOnce(&OsStr, QuotingStyle) -> OsString, -{ - if let Some(locale) = config.locale_quoting { - locale_quote(name, locale) - } else { - fallback(name, config.quoting_style) - } -} - -fn escape_dir_name_with_locale(name: &OsStr, config: &Config) -> OsString { - escape_with_locale(name, config, locale_aware_escape_dir_name) -} - -fn escape_name_with_locale(name: &OsStr, config: &Config) -> OsString { - escape_with_locale(name, config, locale_aware_escape_name) -} - -fn locale_quote(name: &OsStr, style: LocaleQuoting) -> OsString { - let bytes = os_str_as_bytes_lossy(name); - let mut quoted = String::new(); - match style { - LocaleQuoting::Single => quoted.push('\''), - LocaleQuoting::Double => quoted.push('"'), - } - for &byte in bytes.as_ref() { - push_locale_byte(&mut quoted, byte, style); - } - match style { - LocaleQuoting::Single => quoted.push('\''), - LocaleQuoting::Double => quoted.push('"'), - } - OsString::from(quoted) -} - -fn push_locale_byte(buf: &mut String, byte: u8, style: LocaleQuoting) { - match (style, byte) { - (LocaleQuoting::Single, b'\'') => buf.push_str("'\\''"), - (LocaleQuoting::Double, b'"') => buf.push_str("\\\""), - (_, b'\\') => buf.push_str("\\\\"), - _ => push_basic_escape(buf, byte), - } -} - -fn push_basic_escape(buf: &mut String, byte: u8) { - match byte { - b'\x07' => buf.push_str("\\a"), - b'\x08' => buf.push_str("\\b"), - b'\t' => buf.push_str("\\t"), - b'\n' => buf.push_str("\\n"), - b'\x0b' => buf.push_str("\\v"), - b'\x0c' => buf.push_str("\\f"), - b'\r' => buf.push_str("\\r"), - b'\x1b' => buf.push_str("\\e"), - b'"' => buf.push('"'), - b'\'' => buf.push('\''), - b if (0x20..=0x7e).contains(&b) => buf.push(b as char), - _ => { - let _ = write!(buf, "\\{byte:03o}"); - } - } -} - type DirData = (PathBuf, bool); // A struct to encapsulate state that is passed around from `list` functions. +#[cfg_attr(not(unix), allow(dead_code))] struct ListState<'a> { out: BufWriter, style_manager: Option>, @@ -2204,10 +991,15 @@ struct ListState<'a> { uid_cache: FxHashMap, #[cfg(unix)] gid_cache: FxHashMap, + #[cfg(not(unix))] + uid_cache: (), + #[cfg(not(unix))] + gid_cache: (), recent_time_range: RangeInclusive, stack: Vec, listed_ancestors: FxHashSet, initial_locs_len: usize, + display_buf: Vec, } #[allow(clippy::cognitive_complexity)] @@ -2216,6 +1008,7 @@ pub fn list(locs: Vec<&Path>, config: &Config) -> UResult<()> { let mut dirs = Vec::::new(); let mut dired = DiredOutput::default(); let initial_locs_len = locs.len(); + let now = SystemTime::now(); let mut state = ListState { out: BufWriter::new(stdout()), @@ -2224,18 +1017,26 @@ pub fn list(locs: Vec<&Path>, config: &Config) -> UResult<()> { uid_cache: FxHashMap::default(), #[cfg(unix)] gid_cache: FxHashMap::default(), + #[cfg(not(unix))] + uid_cache: (), + #[cfg(not(unix))] + gid_cache: (), // Time range for which to use the "recent" format. Anything from 0.5 year in the past to now // (files with modification time in the future use "old" format). // According to GNU a Gregorian year has 365.2425 * 24 * 60 * 60 == 31556952 seconds on the average. - recent_time_range: (SystemTime::now() - Duration::new(31_556_952 / 2, 0)) - ..=SystemTime::now(), + recent_time_range: (now - Duration::new(31_556_952 / 2, 0))..=now, stack: Vec::new(), listed_ancestors: FxHashSet::default(), initial_locs_len, + display_buf: Vec::with_capacity(if config.format == Format::Long { + 128 + } else { + 0 + }), }; for loc in locs { - let path_data = PathData::new(PathBuf::from(loc), None, None, config, true); + let path_data = PathData::new(loc.into(), None, None, config, true); // Getting metadata here is no big deal as it's just the CWD // and we really just want to know if the strings exist as files/dirs @@ -2345,7 +1146,7 @@ pub fn list(locs: Vec<&Path>, config: &Config) -> UResult<()> { fn sort_entries(entries: &mut [PathData], config: &Config) { match config.sort { - Sort::Time => entries.sort_by_key(|k| { + Sort::Time => entries.sort_unstable_by_key(|k| { Reverse( k.metadata() .and_then(|md| metadata_get_time(md, config.time)) @@ -2353,24 +1154,24 @@ fn sort_entries(entries: &mut [PathData], config: &Config) { ) }), Sort::Size => { - entries.sort_by_key(|k| Reverse(k.metadata().map_or(0, Metadata::len))); + entries.sort_unstable_by_key(|k| Reverse(k.metadata().map_or(0, Metadata::len))); } // The default sort in GNU ls is case insensitive - Sort::Name => entries.sort_by(|a, b| a.display_name().cmp(b.display_name())), - Sort::Version => entries.sort_by(|a, b| { + Sort::Name => entries.sort_unstable_by(|a, b| a.display_name().cmp(b.display_name())), + Sort::Version => entries.sort_unstable_by(|a, b| { version_cmp( os_str_as_bytes_lossy(a.path().as_os_str()).as_ref(), os_str_as_bytes_lossy(b.path().as_os_str()).as_ref(), ) - .then(a.path().to_string_lossy().cmp(&b.path().to_string_lossy())) + .then(a.path().cmp(b.path())) }), - Sort::Extension => entries.sort_by(|a, b| { + Sort::Extension => entries.sort_unstable_by(|a, b| { a.path() .extension() .cmp(&b.path().extension()) .then(a.path().file_stem().cmp(&b.path().file_stem())) }), - Sort::Width => entries.sort_by(|a, b| { + Sort::Width => entries.sort_unstable_by(|a, b| { a.display_name() .len() .cmp(&b.display_name().len()) @@ -2384,1225 +1185,238 @@ fn sort_entries(entries: &mut [PathData], config: &Config) { } if config.group_directories_first && config.sort != Sort::None { - entries.sort_by_key(|p| { + entries.sort_unstable_by_key(|p| { let ft = { // We will always try to deref symlinks to group directories, so PathData.md // is not always useful. if p.must_dereference { - p.file_type() - } else { - None - } - }; - - !match ft { - None => { - // If it metadata cannot be determined, treat as a file. - get_metadata_with_deref_opt(p.p_buf.as_path(), true) - .map_or_else(|_| false, |m| m.is_dir()) - } - Some(ft) => ft.is_dir(), - } - }); - } -} - -fn is_hidden(file_path: &DirEntry) -> bool { - #[cfg(windows)] - { - let metadata = file_path.metadata().unwrap(); - let attr = metadata.file_attributes(); - (attr & 0x2) > 0 - } - #[cfg(not(windows))] - { - file_path.file_name().as_encoded_bytes().starts_with(b".") - } -} - -fn should_display(entry: &DirEntry, config: &Config) -> bool { - // check if hidden - if config.files == Files::Normal && is_hidden(entry) { - return false; - } - - // check if it is among ignore_patterns - let options = MatchOptions { - // setting require_literal_leading_dot to match behavior in GNU ls - require_literal_leading_dot: true, - require_literal_separator: false, - case_sensitive: true, - }; - - let file_name = entry.file_name(); - // 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) => Cow::Borrowed(s), - None => file_name.to_string_lossy(), - }; - - !config - .ignore_patterns - .iter() - .any(|p| p.matches_with(&file_name, options)) -} - -fn depth_first_list( - (dir_path, needs_blank_line): DirData, - mut read_dir: ReadDir, - config: &Config, - state: &mut ListState, - dired: &mut DiredOutput, - is_top_level: bool, -) -> UResult<()> { - let path_data = PathData::new(dir_path, None, None, config, false); - - // Print dir heading - name... 'total' comes after error display - if state.initial_locs_len > 1 || config.recursive { - if is_top_level { - if needs_blank_line { - writeln!(state.out)?; - if config.dired { - dired.padding += 1; - } - } - if config.dired { - dired::indent(&mut state.out)?; - } - show_dir_name(&path_data, &mut state.out, config)?; - writeln!(state.out)?; - if config.dired { - let dir_len = path_data.path().as_os_str().len(); - // add the //SUBDIRED// coordinates - dired::calculate_subdired(dired, dir_len); - // Add the padding for the dir name - dired::add_dir_name(dired, dir_len); - } - } else { - writeln!(state.out)?; - if config.dired { - dired.padding += 1; - dired::indent(&mut state.out)?; - let dir_name_size = path_data.path().as_os_str().len(); - dired::calculate_subdired(dired, dir_name_size); - dired::add_dir_name(dired, dir_name_size); - } - show_dir_name(&path_data, &mut state.out, config)?; - writeln!(state.out)?; - } - } - - // Append entries with initial dot files and record their existence - let (ref mut buf, trim) = if config.files == Files::All { - const DOT_DIRECTORIES: usize = 2; - let v = vec![ - PathData::new( - path_data.path().to_path_buf(), - None, - Some(".".into()), - config, - false, - ), - PathData::new( - path_data.path().join(".."), - None, - Some("..".into()), - config, - false, - ), - ]; - (v, DOT_DIRECTORIES) - } else { - (Vec::new(), 0) - }; - - // Convert those entries to the PathData struct - for raw_entry in read_dir.by_ref() { - match raw_entry { - Ok(dir_entry) => { - if should_display(&dir_entry, config) { - buf.push(PathData::new( - dir_entry.path(), - Some(dir_entry), - None, - config, - false, - )); - } - } - Err(err) => { - state.out.flush()?; - show!(LsError::IOError(err)); - } - } - } - // Relinquish unused space since we won't need it anymore. - buf.shrink_to_fit(); - - sort_entries(buf, config); - - if config.format == Format::Long || config.alloc_size { - let total = return_total(buf, config, &mut state.out)?; - write!(state.out, "{}", total.as_str())?; - if config.dired { - dired::add_total(dired, total.len()); - } - } - - display_items(buf, config, state, dired)?; - - if config.recursive { - for e in buf - .iter() - .skip(trim) - .filter(|p| p.file_type().is_some_and(FileType::is_dir)) - .rev() - { - // Try to open only to report any errors in order to match GNU semantics. - if let Err(err) = fs::read_dir(e.path()) { - state.out.flush()?; - show!(LsError::IOErrorContext( - e.path().to_path_buf(), - err, - e.command_line - )); - } else { - let fi = FileInformation::from_path(e.path(), e.must_dereference)?; - if state.listed_ancestors.insert(fi) { - // Push to stack, but with a less aggressive growth curve. - let (cap, len) = (state.stack.capacity(), state.stack.len()); - if cap == len { - state.stack.reserve_exact(len / 4 + 4); - } - state.stack.push((e.path().to_path_buf(), true)); - } else { - state.out.flush()?; - show!(LsError::AlreadyListedError(e.path().to_path_buf())); - } - } - } - } - Ok(()) -} - -fn get_metadata_with_deref_opt(p_buf: &Path, dereference: bool) -> std::io::Result { - if dereference { - p_buf.metadata() - } else { - p_buf.symlink_metadata() - } -} - -fn display_dir_entry_size( - entry: &PathData, - config: &Config, - state: &mut ListState, -) -> (usize, usize, usize, usize, usize, usize) { - // TODO: Cache/memorize the display_* results so we don't have to recalculate them. - if let Some(md) = entry.metadata() { - let (size_len, major_len, minor_len) = match display_len_or_rdev(md, config) { - SizeOrDeviceId::Device(major, minor) => { - (major.len() + minor.len() + 2usize, major.len(), minor.len()) - } - SizeOrDeviceId::Size(size) => (size.len(), 0usize, 0usize), - }; - ( - display_symlink_count(md).len(), - display_uname(md, config, state).len(), - display_group(md, config, state).len(), - size_len, - major_len, - minor_len, - ) - } else { - (0, 0, 0, 0, 0, 0) - } -} - -// A simple, performant, ExtendPad trait to add a string to a Vec, padding with spaces -// on the left or right, without making additional copies, or using formatting functions. -trait ExtendPad { - fn extend_pad_left(&mut self, string: &str, count: usize); - fn extend_pad_right(&mut self, string: &str, count: usize); -} - -impl ExtendPad for Vec { - fn extend_pad_left(&mut self, string: &str, count: usize) { - if string.len() < count { - self.extend(iter::repeat_n(b' ', count - string.len())); - } - self.extend(string.as_bytes()); - } - - fn extend_pad_right(&mut self, string: &str, count: usize) { - self.extend(string.as_bytes()); - if string.len() < count { - self.extend(iter::repeat_n(b' ', count - string.len())); - } - } -} - -// TODO: Consider converting callers to use ExtendPad instead, as it avoids -// additional copies. -fn pad_left(string: &str, count: usize) -> String { - format!("{string:>count$}") -} - -fn return_total( - items: &[PathData], - config: &Config, - out: &mut BufWriter, -) -> UResult { - let mut total_size = 0; - for item in items { - total_size += item - .metadata() - .as_ref() - .map_or(0, |md| get_block_size(md, config)); - } - if config.dired { - dired::indent(out)?; - } - Ok(format!( - "{}{}", - translate!("ls-total", "size" => display_size(total_size, config)), - config.line_ending - )) -} - -fn display_additional_leading_info( - item: &PathData, - padding: &PaddingCollection, - config: &Config, -) -> String { - let mut result = String::new(); - #[cfg(unix)] - { - if config.inode { - let i = if let Some(md) = item.metadata() { - get_inode(md) - } else { - "?".to_owned() - }; - write!(result, "{} ", pad_left(&i, padding.inode)).unwrap(); - } - } - - if config.alloc_size { - let s = if let Some(md) = item.metadata() { - display_size(get_block_size(md, config), config) - } else { - "?".to_owned() - }; - // extra space is insert to align the sizes, as needed for all formats, except for the comma format. - if config.format == Format::Commas { - write!(result, "{s} ").unwrap(); - } else { - write!(result, "{} ", pad_left(&s, padding.block_size)).unwrap(); - } - } - - result -} - -#[allow(clippy::cognitive_complexity)] -fn display_items( - items: &[PathData], - config: &Config, - state: &mut ListState, - dired: &mut DiredOutput, -) -> UResult<()> { - // `-Z`, `--context`: - // Display the SELinux security context or '?' if none is found. When used with the `-l` - // option, print the security context to the left of the size column. - - let quoted = items.iter().any(|item| { - let name = escape_name_with_locale(item.display_name(), config); - os_str_starts_with(&name, b"'") - }); - - if config.format == Format::Long { - let padding_collection = calculate_padding_collection(items, config, state); - - for item in items { - #[cfg(unix)] - let should_display_leading_info = config.inode || config.alloc_size; - #[cfg(not(unix))] - let should_display_leading_info = config.alloc_size; - - if should_display_leading_info { - let more_info = display_additional_leading_info(item, &padding_collection, config); - - write!(state.out, "{more_info}")?; - } - - display_item_long(item, &padding_collection, config, state, dired, quoted)?; - } - } else { - let mut longest_context_len = 1; - let prefix_context = if config.context { - for item in items { - let context_len = item.security_context(config).len(); - longest_context_len = context_len.max(longest_context_len); - } - Some(longest_context_len) - } else { - None - }; - - let padding = calculate_padding_collection(items, config, state); - - // we need to apply normal color to non filename output - if let Some(style_manager) = &mut state.style_manager { - write!(state.out, "{}", style_manager.apply_normal())?; - } - - let mut names_vec = Vec::new(); - - #[cfg(unix)] - let should_display_leading_info = config.inode || config.alloc_size; - #[cfg(not(unix))] - let should_display_leading_info = config.alloc_size; - - for i in items { - let more_info = if should_display_leading_info { - Some(display_additional_leading_info(i, &padding, config)) - } else { - None - }; - // it's okay to set current column to zero which is used to decide - // whether text will wrap or not, because when format is grid or - // column ls will try to place the item name in a new line if it - // wraps. - let cell = display_item_name( - i, - config, - prefix_context, - more_info, - state, - LazyCell::new(Box::new(|| 0)), - ); - - names_vec.push(cell.displayed); - } - - let mut names = names_vec.into_iter(); - - match config.format { - Format::Columns => { - display_grid( - names, - config.width, - Direction::TopToBottom, - &mut state.out, - quoted, - config.tab_size, - )?; - } - Format::Across => { - display_grid( - names, - config.width, - Direction::LeftToRight, - &mut state.out, - quoted, - config.tab_size, - )?; - } - Format::Commas => { - let mut current_col = 0; - if let Some(name) = names.next() { - write_os_str(&mut state.out, &name)?; - current_col = ansi_width(&name.to_string_lossy()) as u16 + 2; - } - for name in names { - 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; - writeln!(state.out, ",")?; - } else { - current_col += name_width + 2; - write!(state.out, ", ")?; - } - write_os_str(&mut state.out, &name)?; - } - // Current col is never zero again if names have been printed. - // So we print a newline. - if current_col > 0 { - write!(state.out, "{}", config.line_ending)?; - } - } - _ => { - for name in names { - write_os_str(&mut state.out, &name)?; - write!(state.out, "{}", config.line_ending)?; - } - } - } - } - - Ok(()) -} - -#[allow(unused_variables)] -fn get_block_size(md: &Metadata, config: &Config) -> u64 { - /* GNU ls will display sizes in terms of block size - md.len() will differ from this value when the file has some holes - */ - #[cfg(unix)] - { - let raw_blocks = if md.file_type().is_char_device() || md.file_type().is_block_device() { - 0u64 - } else { - md.blocks() * 512 - }; - match config.size_format { - SizeFormat::Binary | SizeFormat::Decimal => raw_blocks, - SizeFormat::Bytes => raw_blocks / config.block_size, - } - } - #[cfg(not(unix))] - { - // no way to get block size for windows, fall-back to file size - md.len() - } -} - -fn display_grid( - names: impl Iterator, - width: u16, - direction: Direction, - out: &mut BufWriter, - quoted: bool, - tab_size: usize, -) -> UResult<()> { - if width == 0 { - // If the width is 0 we print one single line - let mut printed_something = false; - for name in names { - if printed_something { - write!(out, " ")?; - } - printed_something = true; - write_os_str(out, &name)?; - } - if printed_something { - writeln!(out)?; - } - } else { - 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. - // - // Example: - // ``` - // $ ls - // 'a\nb' bar - // foo baz - // ^ ^ - // These spaces is added - // ``` - names - .map(|n| { - if os_str_starts_with(&n, b"'") || os_str_starts_with(&n, b"\"") { - n - } else { - let mut ret: OsString = " ".into(); - ret.push(n); - ret - } - }) - .collect() - } else { - 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(); - - // Since tab_size=0 means no \t, use Spaces separator for optimization. - let filling = match tab_size { - 0 => Filling::Spaces(DEFAULT_SEPARATOR_SIZE), - _ => Filling::Tabs { - spaces: DEFAULT_SEPARATOR_SIZE, - tab_size, - }, - }; - - let grid = Grid::new( - names, - GridOptions { - filling, - direction, - width: width as usize, - }, - ); - write!(out, "{grid}")?; - } - Ok(()) -} - -fn calculate_line_len(output_len: usize, item_len: usize, line_ending: LineEnding) -> usize { - output_len + item_len + line_ending.to_string().len() -} - -fn update_dired_for_item( - dired: &mut DiredOutput, - output_display_len: usize, - displayed_len: usize, - dired_name_len: usize, - line_ending: LineEnding, -) { - let line_len = calculate_line_len(output_display_len, displayed_len, line_ending); - dired::calculate_and_update_positions(dired, output_display_len, dired_name_len, line_len); -} - -/// This writes to the [`BufWriter`] `state.out` a single string of the output of `ls -l`. -/// -/// It writes the following keys, in order: -/// * `inode` ([`get_inode`], config-optional) -/// * `permissions` ([`display_permissions`]) -/// * `symlink_count` ([`display_symlink_count`]) -/// * `owner` ([`display_uname`], config-optional) -/// * `group` ([`display_group`], config-optional) -/// * `author` ([`display_uname`], config-optional) -/// * `size / rdev` ([`display_len_or_rdev`]) -/// * `system_time` ([`display_date`]) -/// * `item_name` ([`display_item_name`]) -/// -/// This function needs to display information in columns: -/// * permissions and `system_time` are already guaranteed to be pre-formatted in fixed length. -/// * `item_name` is the last column and is left-aligned. -/// * Everything else needs to be padded using [`pad_left`]. -/// -/// That's why we have the parameters: -/// ```txt -/// longest_link_count_len: usize, -/// longest_uname_len: usize, -/// longest_group_len: usize, -/// longest_context_len: usize, -/// longest_size_len: usize, -/// ``` -/// that decide the maximum possible character count of each field. -#[allow(clippy::write_literal)] -#[allow(clippy::cognitive_complexity)] -fn display_item_long( - item: &PathData, - padding: &PaddingCollection, - config: &Config, - state: &mut ListState, - dired: &mut DiredOutput, - quoted: bool, -) -> UResult<()> { - let mut output_display: Vec = Vec::with_capacity(128); - - // apply normal color to non filename outputs - if let Some(style_manager) = &mut state.style_manager { - output_display.extend(style_manager.apply_normal().as_bytes()); - } - if config.dired { - output_display.extend(b" "); - } - if let Some(md) = item.metadata() { - #[cfg(any(not(unix), target_os = "android", target_os = "macos"))] - // TODO: See how Mac should work here - let is_acl_set = false; - #[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] - let is_acl_set = has_acl(item.path()); - output_display.extend(display_permissions(md, true).as_bytes()); - if item.security_context(config).len() > 1 { - // GNU `ls` uses a "." character to indicate a file with a security context, - // but not other alternate access method. - output_display.extend(b"."); - } else if is_acl_set { - output_display.extend(b"+"); - } else { - output_display.extend(b" "); - } - - output_display.extend_pad_left(&display_symlink_count(md), padding.link_count); - - if config.long.owner { - output_display.extend(b" "); - output_display.extend_pad_right(display_uname(md, config, state), padding.uname); - } - - if config.long.group { - output_display.extend(b" "); - output_display.extend_pad_right(display_group(md, config, state), padding.group); - } - - if config.context { - output_display.extend(b" "); - output_display.extend_pad_right(item.security_context(config), padding.context); - } - - // Author is only different from owner on GNU/Hurd, so we reuse - // the owner, since GNU/Hurd is not currently supported by Rust. - if config.long.author { - output_display.extend(b" "); - output_display.extend_pad_right(display_uname(md, config, state), padding.uname); - } - - match display_len_or_rdev(md, config) { - SizeOrDeviceId::Size(size) => { - output_display.extend(b" "); - output_display.extend_pad_left(&size, padding.size); - } - SizeOrDeviceId::Device(major, minor) => { - output_display.extend(b" "); - output_display.extend_pad_left( - &major, - #[cfg(not(unix))] - 0usize, - #[cfg(unix)] - padding.major.max( - padding - .size - .saturating_sub(padding.minor.saturating_add(2usize)), - ), - ); - output_display.extend(b", "); - output_display.extend_pad_left( - &minor, - #[cfg(not(unix))] - 0usize, - #[cfg(unix)] - padding.minor, - ); - } - } - - output_display.extend(b" "); - display_date(md, config, state, &mut output_display)?; - output_display.extend(b" "); - - let item_display = display_item_name( - item, - config, - None, - None, - state, - LazyCell::new(Box::new(|| { - ansi_width(&String::from_utf8_lossy(&output_display)) - })), - ); - - let needs_space = quoted && !os_str_starts_with(&item_display.displayed, b"'"); - - if config.dired { - let mut dired_name_len = item_display.dired_name_len; - if needs_space { - dired_name_len += 1; - } - let displayed_len = item_display.displayed.len() + usize::from(needs_space); - update_dired_for_item( - dired, - output_display.len(), - displayed_len, - dired_name_len, - config.line_ending, - ); - } - - let item_name = item_display.displayed; - let displayed_item = if needs_space { - let mut ret: OsString = " ".into(); - ret.push(&item_name); - ret - } else { - item_name - }; - - write_os_str(&mut output_display, &displayed_item)?; - output_display.extend(config.line_ending.to_string().as_bytes()); - } else { - #[cfg(unix)] - let leading_char = { - if let Some(ft) = item.file_type() { - if ft.is_char_device() { - "c" - } else if ft.is_block_device() { - "b" - } else if ft.is_symlink() { - "l" - } else if ft.is_dir() { - "d" - } else { - "-" - } - } else if item.is_dangling_link() { - "l" - } else { - "-" - } - }; - #[cfg(not(unix))] - let leading_char = { - if let Some(ft) = item.file_type() { - if ft.is_symlink() { - "l" - } else if ft.is_dir() { - "d" - } else { - "-" - } - } else if item.is_dangling_link() { - "l" - } else { - "-" - } - }; - - output_display.extend(leading_char.as_bytes()); - output_display.extend(b"?????????"); - if item.security_context(config).len() > 1 { - // GNU `ls` uses a "." character to indicate a file with a security context, - // but not other alternate access method. - output_display.extend(b"."); - } - output_display.extend(b" "); - output_display.extend_pad_left("?", padding.link_count); - - if config.long.owner { - output_display.extend(b" "); - output_display.extend_pad_right("?", padding.uname); - } - - if config.long.group { - output_display.extend(b" "); - output_display.extend_pad_right("?", padding.group); - } - - if config.context { - output_display.extend(b" "); - output_display.extend_pad_right(item.security_context(config), padding.context); - } - - // Author is only different from owner on GNU/Hurd, so we reuse - // the owner, since GNU/Hurd is not currently supported by Rust. - if config.long.author { - output_display.extend(b" "); - output_display.extend_pad_right("?", padding.uname); - } - - let displayed_item = display_item_name( - item, - config, - None, - None, - state, - LazyCell::new(Box::new(|| { - ansi_width(&String::from_utf8_lossy(&output_display)) - })), - ); - let date_len = 12; - - output_display.extend(b" "); - output_display.extend_pad_left("?", padding.size); - output_display.extend(b" "); - output_display.extend_pad_left("?", date_len); - output_display.extend(b" "); - - if config.dired { - update_dired_for_item( - dired, - output_display.len(), - displayed_item.displayed.len(), - displayed_item.dired_name_len, - config.line_ending, - ); - } - let displayed_item = displayed_item.displayed; - write_os_str(&mut output_display, &displayed_item)?; - output_display.extend(config.line_ending.to_string().as_bytes()); - } - state.out.write_all(&output_display)?; - - Ok(()) -} - -#[cfg(unix)] -fn get_inode(metadata: &Metadata) -> String { - format!("{}", metadata.ino()) -} - -// Currently getpwuid is `linux` target only. If it's broken state.out into -// a posix-compliant attribute this can be updated... -#[cfg(unix)] -fn display_uname<'a>(metadata: &Metadata, config: &Config, state: &'a mut ListState) -> &'a String { - let uid = metadata.uid(); - - state.uid_cache.entry(uid).or_insert_with(|| { - if config.long.numeric_uid_gid { - uid.to_string() - } else { - entries::uid2usr(uid).unwrap_or_else(|_| uid.to_string()) - } - }) -} - -#[cfg(unix)] -fn display_group<'a>(metadata: &Metadata, config: &Config, state: &'a mut ListState) -> &'a String { - let gid = metadata.gid(); - state.gid_cache.entry(gid).or_insert_with(|| { - if config.long.numeric_uid_gid { - gid.to_string() - } else { - entries::gid2grp(gid).unwrap_or_else(|_| gid.to_string()) - } - }) -} - -#[cfg(not(unix))] -fn display_uname(_metadata: &Metadata, _config: &Config, _state: &mut ListState) -> &'static str { - "somebody" -} - -#[cfg(not(unix))] -fn display_group(_metadata: &Metadata, _config: &Config, _state: &mut ListState) -> &'static str { - "somegroup" -} - -fn display_date( - metadata: &Metadata, - config: &Config, - state: &mut ListState, - out: &mut Vec, -) -> UResult<()> { - let Some(time) = metadata_get_time(metadata, config.time) else { - out.extend(b"???"); - return Ok(()); - }; - - // Use "recent" format if the given date is considered recent (i.e., in the last 6 months), - // or if no "older" format is available. - let fmt = match &config.time_format_older { - Some(time_format_older) if !state.recent_time_range.contains(&time) => time_format_older, - _ => &config.time_format_recent, - }; - - format_system_time(out, time, fmt, FormatSystemTimeFallback::Integer) -} - -#[allow(dead_code)] -enum SizeOrDeviceId { - Size(String), - Device(String, String), -} - -fn display_len_or_rdev(metadata: &Metadata, config: &Config) -> SizeOrDeviceId { - #[cfg(any( - target_os = "linux", - target_os = "macos", - target_os = "android", - target_os = "ios", - target_os = "freebsd", - target_os = "dragonfly", - target_os = "netbsd", - target_os = "openbsd", - target_os = "illumos", - target_os = "solaris" - ))] - { - let ft = metadata.file_type(); - if ft.is_char_device() || ft.is_block_device() { - // A type cast is needed here as the `dev_t` type varies across OSes. - let dev = metadata.rdev() as dev_t; - let major = major(dev); - let minor = minor(dev); - return SizeOrDeviceId::Device(major.to_string(), minor.to_string()); - } - } - let len_adjusted = { - let d = metadata.len() / config.file_size_block_size; - let r = metadata.len() % config.file_size_block_size; - if r == 0 { d } else { d + 1 } - }; - SizeOrDeviceId::Size(display_size(len_adjusted, config)) -} - -fn display_size(size: u64, config: &Config) -> String { - human_readable(size, config.size_format) -} - -#[cfg(unix)] -fn file_is_executable(md: &Metadata) -> bool { - // Mode always returns u32, but the flags might not be, based on the platform - // e.g. linux has u32, mac has u16. - // S_IXUSR -> user has execute permission - // S_IXGRP -> group has execute permission - // S_IXOTH -> other users have execute permission - #[allow(clippy::unnecessary_cast)] - return md.mode() & ((S_IXUSR | S_IXGRP | S_IXOTH) as u32) != 0; -} - -fn classify_file(path: &PathData) -> Option { - let file_type = path.file_type()?; + p.file_type() + } else { + None + } + }; - if file_type.is_dir() { - Some('/') - } else if file_type.is_symlink() { - Some('@') - } else { - #[cfg(unix)] - { - if file_type.is_socket() { - Some('=') - } else if file_type.is_fifo() { - Some('|') - // Safe unwrapping if the file was removed between listing and display - // See https://github.com/uutils/coreutils/issues/5371 - } else if path.is_executable_file() { - Some('*') - } else { - None + !match ft { + None => { + // If it metadata cannot be determined, treat as a file. + get_metadata_with_deref_opt(&p.p_buf, true) + .map_or_else(|_| false, |m| m.is_dir()) + } + Some(ft) => ft.is_dir(), } - } - #[cfg(not(unix))] - None + }); } } -/// Takes a [`PathData`] struct and returns a cell with a name ready for displaying. -/// -/// This function relies on the following parameters in the provided `&Config`: -/// * `config.quoting_style` to decide how we will escape `name` using [`locale_aware_escape_name`]. -/// * `config.inode` decides whether to display inode numbers beside names using [`get_inode`]. -/// * `config.color` decides whether it's going to color `name` using [`color_name`]. -/// * `config.indicator_style` to append specific characters to `name` using [`classify_file`]. -/// * `config.format` to display symlink targets if `Format::Long`. This function is also -/// responsible for coloring symlink target names if `config.color` is specified. -/// * `config.context` to prepend security context to `name` if compiled with `feat_selinux`. -/// * `config.hyperlink` decides whether to hyperlink the item -/// -/// Note that non-unicode sequences in symlink targets are dealt with using -/// [`std::path::Path::to_string_lossy`]. -#[allow(clippy::cognitive_complexity)] -fn display_item_name( - path: &PathData, +fn depth_first_list( + (dir_path, needs_blank_line): DirData, + mut read_dir: ReadDir, config: &Config, - prefix_context: Option, - more_info: Option, state: &mut ListState, - current_column: LazyCell usize + '_>>, -) -> DisplayItemName { - // This is our return value. We start by `&path.display_name` and modify it along the way. - let mut name = escape_name_with_locale(path.display_name(), config); - - let is_wrap = - |namelen: usize| config.width != 0 && *current_column + namelen > config.width.into(); - - if config.hyperlink { - name = create_hyperlink(&name, path); - } - - if let Some(style_manager) = &mut state.style_manager { - let len = name.len(); - name = color_name(name, path, style_manager, None, is_wrap(len)); - } - - if config.format != Format::Long { - if let Some(info) = more_info { - let old_name = name; - name = info.into(); - name.push(&old_name); - } - } - - if config.indicator_style != IndicatorStyle::None { - let sym = classify_file(path); + dired: &mut DiredOutput, + is_top_level: bool, +) -> UResult<()> { + let path_data = PathData::new(dir_path.as_path().into(), None, None, config, false); - let char_opt = match config.indicator_style { - IndicatorStyle::Classify => sym, - IndicatorStyle::FileType => { - // Don't append an asterisk. - match sym { - Some('*') => None, - _ => sym, + // Print dir heading - name... 'total' comes after error display + if state.initial_locs_len > 1 || config.recursive { + if is_top_level { + if needs_blank_line { + writeln!(state.out)?; + if config.dired { + dired.padding += 1; } } - IndicatorStyle::Slash => { - // Append only a slash. - match sym { - Some('/') => Some('/'), - _ => None, - } + if config.dired { + dired::indent(&mut state.out)?; } - IndicatorStyle::None => None, - }; - - if let Some(c) = char_opt { - name.push(OsStr::new(&c.to_string())); + show_dir_name(&path_data, &mut state.out, config)?; + writeln!(state.out)?; + if config.dired { + let dir_len = path_data.path().as_os_str().len(); + // add the //SUBDIRED// coordinates + dired::calculate_subdired(dired, dir_len); + // Add the padding for the dir name + dired::add_dir_name(dired, dir_len); + } + } else { + writeln!(state.out)?; + if config.dired { + dired.padding += 1; + dired::indent(&mut state.out)?; + let dir_name_size = path_data.path().as_os_str().len(); + dired::calculate_subdired(dired, dir_name_size); + dired::add_dir_name(dired, dir_name_size); + } + show_dir_name(&path_data, &mut state.out, config)?; + writeln!(state.out)?; } } - let dired_name_len = if config.dired { name.len() } else { 0 }; - - if config.format == Format::Long - && path.file_type().is_some_and(FileType::is_symlink) - && !path.must_dereference - { - match path.path().read_link() { - Ok(target_path) => { - name.push(" -> "); - - // We might as well color the symlink output after the arrow. - // This makes extra system calls, but provides important information that - // people run `ls -l --color` are very interested in. - if let Some(style_manager) = &mut state.style_manager { - let escaped_target = escape_name_with_locale(target_path.as_os_str(), config); - // We get the absolute path to be able to construct PathData with valid Metadata. - // This is because relative symlinks will fail to get_metadata. - let mut absolute_target = target_path.clone(); - if target_path.is_relative() { - if let Some(parent) = path.path().parent() { - absolute_target = parent.join(absolute_target); - } - } + // Append entries with initial dot files and record their existence + let (ref mut buf, trim) = if config.files == Files::All { + const DOT_DIRECTORIES: usize = 2; + let v = vec![ + PathData::new( + path_data.path().into(), + None, + Some(OsStr::new(".").into()), + config, + false, + ), + PathData::new( + // On WASI the sandbox may block access to ".." at the + // preopened root. Fall back to "." so the entry still + // appears with valid metadata instead of an error. + { + let dotdot = path_data.path().join(".."); + #[cfg(target_os = "wasi")] + let dotdot = if dotdot.metadata().is_err() { + path_data.path().into() + } else { + dotdot + }; + dotdot.into() + }, + None, + Some(OsStr::new("..").into()), + config, + false, + ), + ]; + (v, DOT_DIRECTORIES) + } else { + (Vec::new(), 0) + }; - match fs::canonicalize(&absolute_target) { - Ok(resolved_target) => { - let target_data = PathData::new( - resolved_target, - None, - target_path.file_name().map(OsStr::to_os_string), - config, - false, - ); - - // Check if the target actually needs coloring - let md_option: Option = target_data - .metadata() - .cloned() - .or_else(|| target_data.p_buf.symlink_metadata().ok()); - let style = style_manager.colors.style_for_path_with_metadata( - &target_data.p_buf, - md_option.as_ref(), - ); - - if style.is_some() { - // Only apply coloring if there's actually a style - name.push(color_name( - escaped_target, - &target_data, - style_manager, - None, - is_wrap(name.len()), - )); - } else { - // For regular files with no coloring, just use plain text - name.push(escaped_target); - } - } - Err(_) => { - name.push( - style_manager.apply_missing_target_style( - escaped_target, - is_wrap(name.len()), - ), - ); - } - } - } else { - // If no coloring is required, we just use target as is. - // Apply the right quoting - name.push(escape_name_with_locale(target_path.as_os_str(), config)); + // Convert those entries to the PathData struct + for raw_entry in read_dir.by_ref() { + match raw_entry { + Ok(dir_entry) => { + if should_display(&dir_entry, config) { + buf.push(PathData::new( + dir_entry.path().into(), + Some(dir_entry), + None, + config, + false, + )); } } Err(err) => { - show!(LsError::IOErrorContext( - path.path().to_path_buf(), - err, - false - )); + state.out.flush()?; + show!(LsError::IOError(err)); } } } + // Relinquish unused space since we won't need it anymore. + buf.shrink_to_fit(); - // Prepend the security context to the `name` and adjust `width` in order - // to get correct alignment from later calls to`display_grid()`. - if config.context { - if let Some(pad_count) = prefix_context { - let security_context = if matches!(config.format, Format::Commas) { - path.security_context(config).to_string() - } else { - pad_left(path.security_context(config), pad_count) - }; + sort_entries(buf, config); - let old_name = name; - name = format!("{security_context} ").into(); - name.push(old_name); + if config.format == Format::Long || config.alloc_size { + let total = write_total(buf, config, &mut state.out)?; + if config.dired { + dired::add_total(dired, total); } } - DisplayItemName { - displayed: name, - dired_name_len, - } -} - -fn create_hyperlink(name: &OsStr, path: &PathData) -> OsString { - let hostname = hostname::get().unwrap_or_else(|_| OsString::from("")); - let hostname = hostname.to_string_lossy(); - - let absolute_path = fs::canonicalize(path.path()).unwrap_or_default(); - - // Get bytes for URL encoding in a cross-platform way - let absolute_path_bytes = os_str_as_bytes_lossy(absolute_path.as_os_str()); - - // a set of safe ASCII bytes that don't need encoding - #[cfg(not(target_os = "windows"))] - let unencoded_bytes = b"_-.~/"; - #[cfg(target_os = "windows")] - let unencoded_bytes = b"_-.~/\\:"; + display_items(buf, config, state, dired)?; - // Encode at byte level to properly handle UTF-8 sequences and preserve invalid UTF-8 - let full_encoded_path: String = absolute_path_bytes - .iter() - .map(|&b: &u8| { - if b.is_ascii_alphanumeric() || unencoded_bytes.contains(&b) { - (b as char).to_string() + if config.recursive { + for e in buf + .iter() + .skip(trim) + .filter(|p| p.file_type().is_some_and(FileType::is_dir)) + .rev() + { + // Try to open only to report any errors in order to match GNU semantics. + if let Err(err) = fs::read_dir(e.path()) { + state.out.flush()?; + show!(LsError::IOErrorContext( + e.path().to_path_buf(), + err, + e.command_line + )); } else { - format!("%{b:02x}") + let fi = FileInformation::from_path(e.path(), e.must_dereference)?; + if state.listed_ancestors.insert(fi) { + // Push to stack, but with a less aggressive growth curve. + let (cap, len) = (state.stack.capacity(), state.stack.len()); + if cap == len { + state.stack.reserve_exact(len / 4 + 4); + } + state.stack.push((e.path().to_path_buf(), true)); + } else { + state.out.flush()?; + show!(LsError::AlreadyListedError(e.path().to_path_buf())); + } } - }) - .collect(); - - // OSC 8 hyperlink format: \x1b]8;;URL\x1b\\TEXT\x1b]8;;\x1b\\ - // \x1b = ESC, \x1b\\ = ESC backslash - let mut ret: OsString = format!("\x1b]8;;file://{hostname}{full_encoded_path}\x1b\\").into(); - ret.push(name); - ret.push("\x1b]8;;\x1b\\"); + } + } + Ok(()) +} - ret +fn get_metadata_with_deref_opt(p_buf: &Path, dereference: bool) -> std::io::Result { + if dereference { + p_buf.metadata() + } else { + p_buf.symlink_metadata() + } } -#[cfg(not(unix))] -fn display_symlink_count(_metadata: &Metadata) -> String { - // Currently not sure of how to get this on Windows, so I'm punting. - // Git Bash looks like it may do the same thing. - String::from("1") +fn write_total(items: &[PathData], config: &Config, out: &mut BufWriter) -> UResult { + let mut total_size = 0; + for item in items { + total_size += item + .metadata() + .as_ref() + .map_or(0, |md| get_block_size(md, config)); + } + if config.dired { + dired::indent(out)?; + } + let total = translate!("ls-total", "size" => display_size(total_size, config)); + out.write_all(total.as_bytes())?; + out.write_all(&[config.line_ending as u8])?; + Ok(total.len() + 1) } -#[cfg(unix)] -fn display_symlink_count(metadata: &Metadata) -> String { - metadata.nlink().to_string() +#[allow(unused_variables)] +fn get_block_size(md: &Metadata, config: &Config) -> u64 { + /* GNU ls will display sizes in terms of block size + md.len() will differ from this value when the file has some holes + */ + #[cfg(unix)] + { + use uucore::format::human::SizeFormat; + + let raw_blocks = if md.file_type().is_char_device() || md.file_type().is_block_device() { + 0u64 + } else { + md.blocks() * 512 + }; + match config.size_format { + SizeFormat::Binary | SizeFormat::Decimal => raw_blocks, + SizeFormat::Bytes => raw_blocks / config.block_size, + } + } + #[cfg(not(unix))] + { + // no way to get block size for windows, fall-back to file size + md.len() + } } #[cfg(unix)] -fn display_inode(metadata: &Metadata) -> String { - get_inode(metadata) +fn file_is_executable(md: &Metadata) -> bool { + // Mode always returns u32, but the flags might not be, based on the platform + // e.g. linux has u32, mac has u16. + // S_IXUSR -> user has execute permission + // S_IXGRP -> group has execute permission + // S_IXOTH -> other users have execute permission + #[allow(clippy::unnecessary_cast)] + return md.mode() & ((S_IXUSR | S_IXGRP | S_IXOTH) as u32) != 0; } /// This returns the `SELinux` security context as UTF8 `String`. @@ -3631,6 +1445,8 @@ fn get_security_context<'a>( #[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))] if config.selinux_supported { + use uucore::show_warning; + match selinux::SecurityContext::of_path(path, must_dereference, false) { Err(_r) => { // TODO: show the actual reason why it failed @@ -3638,7 +1454,7 @@ fn get_security_context<'a>( "{}", translate!( "ls-warning-failed-to-get-security-context", - "path" => path.quote().to_string() + "path" => path.quote() ) ); return Cow::Borrowed(SUBSTITUTE_STRING); @@ -3649,18 +1465,20 @@ fn get_security_context<'a>( let context = context.strip_suffix(&[0]).unwrap_or(context); - let res: String = String::from_utf8(context.to_vec()).unwrap_or_else(|e| { - show_warning!( - "{}", - translate!( - "ls-warning-getting-security-context", - "path" => path.quote().to_string(), - "error" => e.to_string() - ) - ); - - String::from_utf8_lossy(context).to_string() - }); + let res: String = match str::from_utf8(context) { + Ok(s) => s.to_string(), + Err(e) => { + show_warning!( + "{}", + translate!( + "ls-warning-getting-security-context", + "path" => path.quote(), + "error" => e + ) + ); + String::from_utf8_lossy(context).into_owned() + } + }; return Cow::Owned(res); } @@ -3683,125 +1501,3 @@ fn get_security_context<'a>( Cow::Borrowed(SUBSTITUTE_STRING) } - -#[cfg(unix)] -fn calculate_padding_collection( - items: &[PathData], - config: &Config, - state: &mut ListState, -) -> PaddingCollection { - let mut padding_collections = PaddingCollection { - inode: 1, - link_count: 1, - uname: 1, - group: 1, - context: 1, - size: 1, - major: 1, - minor: 1, - block_size: 1, - }; - - for item in items { - #[cfg(unix)] - if config.inode { - let inode_len = if let Some(md) = item.metadata() { - display_inode(md).len() - } else { - continue; - }; - padding_collections.inode = inode_len.max(padding_collections.inode); - } - - if config.alloc_size { - if let Some(md) = item.metadata() { - let block_size_len = display_size(get_block_size(md, config), config).len(); - padding_collections.block_size = block_size_len.max(padding_collections.block_size); - } - } - - if config.format == Format::Long { - let context_len = item.security_context(config).len(); - let (link_count_len, uname_len, group_len, size_len, major_len, minor_len) = - display_dir_entry_size(item, config, state); - padding_collections.link_count = link_count_len.max(padding_collections.link_count); - padding_collections.uname = uname_len.max(padding_collections.uname); - padding_collections.group = group_len.max(padding_collections.group); - if config.context { - padding_collections.context = context_len.max(padding_collections.context); - } - - // correctly align columns when some files have capabilities/ACLs and others do not - { - #[cfg(any(not(unix), target_os = "android", target_os = "macos"))] - // TODO: See how Mac should work here - let is_acl_set = false; - #[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] - let is_acl_set = has_acl(item.display_name()); - if context_len > 1 || is_acl_set { - padding_collections.link_count += 1; - } - } - - if items.len() == 1usize { - padding_collections.size = 0usize; - padding_collections.major = 0usize; - padding_collections.minor = 0usize; - } else { - padding_collections.major = major_len.max(padding_collections.major); - padding_collections.minor = minor_len.max(padding_collections.minor); - padding_collections.size = size_len - .max(padding_collections.size) - .max(padding_collections.major); - } - } - } - - padding_collections -} - -#[cfg(not(unix))] -fn calculate_padding_collection( - items: &[PathData], - config: &Config, - state: &mut ListState, -) -> PaddingCollection { - let mut padding_collections = PaddingCollection { - link_count: 1, - uname: 1, - group: 1, - context: 1, - size: 1, - block_size: 1, - }; - - for item in items { - if config.alloc_size { - if let Some(md) = item.metadata() { - let block_size_len = display_size(get_block_size(md, config), config).len(); - padding_collections.block_size = block_size_len.max(padding_collections.block_size); - } - } - - let context_len = item.security_context(config).len(); - let (link_count_len, uname_len, group_len, size_len, _major_len, _minor_len) = - display_dir_entry_size(item, config, state); - padding_collections.link_count = link_count_len.max(padding_collections.link_count); - padding_collections.uname = uname_len.max(padding_collections.uname); - padding_collections.group = group_len.max(padding_collections.group); - if config.context { - padding_collections.context = context_len.max(padding_collections.context); - } - padding_collections.size = size_len.max(padding_collections.size); - } - - 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 6e2e082d044..394bbbeac4d 100644 --- a/src/uu/mkdir/Cargo.toml +++ b/src/uu/mkdir/Cargo.toml @@ -23,6 +23,9 @@ clap = { workspace = true } uucore = { workspace = true, features = ["fs", "mode", "fsxattr"] } fluent = { workspace = true } +[target.'cfg(unix)'.dependencies] +rustix = { workspace = true, features = ["process", "fs"] } + [features] selinux = ["uucore/selinux"] smack = ["uucore/smack"] diff --git a/src/uu/mkdir/src/mkdir.rs b/src/uu/mkdir/src/mkdir.rs index 3d0a9bc33c9..9d632115c54 100644 --- a/src/uu/mkdir/src/mkdir.rs +++ b/src/uu/mkdir/src/mkdir.rs @@ -104,9 +104,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("mkdir") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("mkdir")) .about(translate!("mkdir-about")) .override_usage(format_usage(&translate!("mkdir-usage"))) .infer_long_args(true) @@ -252,13 +252,13 @@ fn create_dir(path: &Path, is_parent: bool, config: &Config) -> UResult<()> { /// RAII guard to restore umask on drop, ensuring cleanup even on panic. #[cfg(unix)] -struct UmaskGuard(uucore::libc::mode_t); +struct UmaskGuard(rustix::fs::Mode); #[cfg(unix)] impl UmaskGuard { /// Set umask to the given value and return a guard that restores the original on drop. - fn set(new_mask: uucore::libc::mode_t) -> Self { - let old_mask = unsafe { uucore::libc::umask(new_mask) }; + fn set(new_mask: rustix::fs::Mode) -> Self { + let old_mask = rustix::process::umask(new_mask); Self(old_mask) } } @@ -266,9 +266,7 @@ impl UmaskGuard { #[cfg(unix)] impl Drop for UmaskGuard { fn drop(&mut self) { - unsafe { - uucore::libc::umask(self.0); - } + rustix::process::umask(self.0); } } @@ -283,7 +281,7 @@ fn create_dir_with_mode(path: &Path, mode: u32) -> std::io::Result<()> { // Temporarily set umask to 0 so the directory is created with the exact mode. // The guard restores the original umask on drop, even if we panic. - let _guard = UmaskGuard::set(0); + let _guard = UmaskGuard::set(rustix::fs::Mode::empty()); std::fs::DirBuilder::new().mode(mode).create(path) } @@ -316,7 +314,7 @@ fn create_single_dir(path: &Path, is_parent: bool, config: &Config) -> UResult<( writeln!( stdout(), "{}", - translate!("mkdir-verbose-created-directory", "util_name" => uucore::util_name(), "path" => path.quote()) + translate!("mkdir-verbose-created-directory", "util_name" => "mkdir", "path" => path.quote()) )?; } @@ -366,7 +364,7 @@ fn create_single_dir(path: &Path, is_parent: bool, config: &Config) -> UResult<( writeln!( stdout(), "{}", - translate!("mkdir-verbose-created-directory", "util_name" => uucore::util_name(), "path" => path.quote()) + translate!("mkdir-verbose-created-directory", "util_name" => "mkdir", "path" => path.quote()) )?; } Ok(()) diff --git a/src/uu/mkfifo/src/mkfifo.rs b/src/uu/mkfifo/src/mkfifo.rs index a70d140c7c6..a48a4894ee6 100644 --- a/src/uu/mkfifo/src/mkfifo.rs +++ b/src/uu/mkfifo/src/mkfifo.rs @@ -97,9 +97,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("mkfifo") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("mkfifo")) .override_usage(format_usage(&translate!("mkfifo-usage"))) .about(translate!("mkfifo-about")) .infer_long_args(true) diff --git a/src/uu/mknod/src/mknod.rs b/src/uu/mknod/src/mknod.rs index 9b6ab45f648..39ac62cb62d 100644 --- a/src/uu/mknod/src/mknod.rs +++ b/src/uu/mknod/src/mknod.rs @@ -104,7 +104,8 @@ fn mknod(file_name: &str, config: Config) -> i32 { ) { // if it fails, delete the file let _ = std::fs::remove_file(file_name); - eprintln!("{}: {e}", uucore::util_name()); + use std::io::{Write, stderr}; + let _ = writeln!(stderr(), "mknod: {e}"); return 1; } } @@ -117,7 +118,8 @@ fn mknod(file_name: &str, config: Config) -> i32 { std::fs::remove_file(p) }) { - eprintln!("{}: {e}", uucore::util_name()); + use std::io::{Write, stderr}; + let _ = writeln!(stderr(), "mknod: {e}"); return 1; } } @@ -189,9 +191,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("mknod") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("mknod")) .override_usage(format_usage(&translate!("mknod-usage"))) .after_help(translate!("mknod-after-help")) .about(translate!("mknod-about")) diff --git a/src/uu/mktemp/src/mktemp.rs b/src/uu/mktemp/src/mktemp.rs index f45c676bede..f0cd78cfe55 100644 --- a/src/uu/mktemp/src/mktemp.rs +++ b/src/uu/mktemp/src/mktemp.rs @@ -434,9 +434,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("mktemp") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("mktemp")) .about(translate!("mktemp-about")) .override_usage(format_usage(&translate!("mktemp-usage"))) .infer_long_args(true) diff --git a/src/uu/more/src/more.rs b/src/uu/more/src/more.rs index f31521a6e08..0ad3006024c 100644 --- a/src/uu/more/src/more.rs +++ b/src/uu/more/src/more.rs @@ -206,11 +206,11 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("more") .about(translate!("more-about")) .override_usage(format_usage(&translate!("more-usage"))) .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("more")) .infer_long_args(true) .arg( Arg::new(options::SILENT) diff --git a/src/uu/mv/locales/fr-FR.ftl b/src/uu/mv/locales/fr-FR.ftl index 9ea2f2114b1..2c7a279b74b 100644 --- a/src/uu/mv/locales/fr-FR.ftl +++ b/src/uu/mv/locales/fr-FR.ftl @@ -47,7 +47,7 @@ mv-help-target-directory = déplacer tous les arguments SOURCE dans RÉPERTOIRE mv-help-no-target-directory = traiter DEST comme un fichier normal mv-help-verbose = expliquer ce qui est fait mv-help-progress = Afficher une barre de progression. - Note : cette fonctionnalité n'est pas supportée par GNU coreutils. + Note : cette fonctionnalité n'est pas prise en charge par GNU coreutils. mv-help-debug = expliquer comment un fichier est copié. Implique -v # Messages verbeux diff --git a/src/uu/mv/src/mv.rs b/src/uu/mv/src/mv.rs index 517e70a4fc6..b5f789ff0ba 100644 --- a/src/uu/mv/src/mv.rs +++ b/src/uu/mv/src/mv.rs @@ -226,7 +226,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("mv") .version(uucore::crate_version!()) .about(translate!("mv-about")) .help_template(uucore::localized_help_template(uucore::util_name())) @@ -821,6 +821,8 @@ fn rename_with_fallback( const EXDEV: i32 = windows_sys::Win32::Foundation::ERROR_NOT_SAME_DEVICE as _; #[cfg(unix)] const EXDEV: i32 = libc::EXDEV as _; + #[cfg(target_os = "wasi")] + const EXDEV: i32 = 18; // POSIX EXDEV value // We will only copy if: // 1. Files are on different devices (EXDEV error) @@ -926,13 +928,9 @@ fn rename_symlink_fallback(from: &Path, to: &Path) -> io::Result<()> { } } -#[cfg(not(any(windows, unix)))] -fn rename_symlink_fallback(from: &Path, to: &Path) -> io::Result<()> { - let path_symlink_points_to = fs::read_link(from)?; - Err(io::Error::new( - io::ErrorKind::Other, - translate!("mv-error-no-symlink-support"), - )) +#[cfg(target_os = "wasi")] +fn rename_symlink_fallback(_from: &Path, _to: &Path) -> io::Result<()> { + Err(io::Error::other(translate!("mv-error-no-symlink-support"))) } fn rename_dir_fallback( @@ -1255,14 +1253,13 @@ fn is_writable(path: &Path) -> (bool, Option) { #[cfg(unix)] fn get_interactive_prompt(to: &Path, cached_mode: Option) -> String { - use libc::mode_t; // Use cached mode if available, otherwise fetch it let mode = cached_mode.or_else(|| to.metadata().ok().map(|m| m.permissions().mode())); if let Some(mode) = mode { let file_mode = mode & 0o777; // Check if file is not writable by user if (mode & 0o200) == 0 { - let perms = display_permissions_unix(mode as mode_t, false); + let perms = display_permissions_unix(mode, false); let mode_info = format!("{file_mode:04o} ({perms})"); return translate!("mv-prompt-overwrite-mode", "target" => to.quote(), "mode_info" => mode_info); } diff --git a/src/uu/nice/Cargo.toml b/src/uu/nice/Cargo.toml index 58dfb69f6af..45703c435d1 100644 --- a/src/uu/nice/Cargo.toml +++ b/src/uu/nice/Cargo.toml @@ -20,12 +20,11 @@ path = "src/nice.rs" [dependencies] clap = { workspace = true } -libc = { workspace = true } uucore = { workspace = true } fluent = { workspace = true } [target.'cfg(unix)'.dependencies] -nix = { workspace = true } +rustix = { workspace = true, features = ["process"] } [[bin]] name = "nice" diff --git a/src/uu/nice/src/nice.rs b/src/uu/nice/src/nice.rs index 1abb7e96ff1..298bef3ba03 100644 --- a/src/uu/nice/src/nice.rs +++ b/src/uu/nice/src/nice.rs @@ -6,9 +6,8 @@ // spell-checker:ignore (ToDO) getpriority setpriority nstr PRIO use clap::{Arg, ArgAction, Command}; -use libc::PRIO_PROCESS; use std::ffi::OsString; -use std::io::{Error, ErrorKind, Write, stdout}; +use std::io::{ErrorKind, Write, stdout}; use std::num::IntErrorKind; use std::os::unix::process::CommandExt; use std::process; @@ -110,14 +109,12 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uucore::clap_localization::handle_clap_result_with_exit_code(uu_app(), args, 125)?; - nix::errno::Errno::clear(); - let mut niceness = unsafe { libc::getpriority(PRIO_PROCESS, 0) }; - if Error::last_os_error().raw_os_error().unwrap() != 0 { - return Err(USimpleError::new( - 125, - format!("getpriority: {}", Error::last_os_error()), - )); - } + let mut niceness = match rustix::process::getpriority_process(None) { + Ok(p) => p, + Err(e) => { + return Err(USimpleError::new(125, format!("getpriority: {e}"))); + } + }; let adjustment = if let Some(nstr) = matches.get_one::(options::ADJUSTMENT) { if !matches.contains_id(options::COMMAND) { @@ -152,8 +149,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // isn't writable. The GNU test suite checks specifically that the // exit code when failing to write the advisory is 125, but Rust // will produce an exit code of 101 when it panics. - if unsafe { libc::setpriority(PRIO_PROCESS, 0, niceness) } == -1 { - let warning_msg = translate!("nice-warning-setpriority", "util_name" => uucore::util_name(), "error" => Error::last_os_error()); + if let Err(e) = rustix::process::setpriority_process(None, niceness) { + let warning_msg = translate!("nice-warning-setpriority", "util_name" => "nice", "error" => e.to_string() ); if write!(std::io::stderr(), "{warning_msg}").is_err() { set_exit_code(125); @@ -179,13 +176,13 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("nice") .about(translate!("nice-about")) .override_usage(format_usage(&translate!("nice-usage"))) .trailing_var_arg(true) .infer_long_args(true) .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("nice")) .arg( Arg::new(options::ADJUSTMENT) .short('n') diff --git a/src/uu/nl/src/nl.rs b/src/uu/nl/src/nl.rs index f32bed18b2c..b9d62c4a7e4 100644 --- a/src/uu/nl/src/nl.rs +++ b/src/uu/nl/src/nl.rs @@ -265,7 +265,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("nl") .about(translate!("nl-about")) .version(uucore::crate_version!()) .help_template(uucore::localized_help_template(uucore::util_name())) diff --git a/src/uu/nohup/src/nohup.rs b/src/uu/nohup/src/nohup.rs index c09872c8829..ccf14d2e720 100644 --- a/src/uu/nohup/src/nohup.rs +++ b/src/uu/nohup/src/nohup.rs @@ -14,6 +14,7 @@ use std::os::unix::prelude::*; use std::os::unix::process::CommandExt; use std::path::{Path, PathBuf}; use std::process; +use std::sync::LazyLock; use thiserror::Error; use uucore::display::Quotable; use uucore::error::{UError, UResult, set_exit_code}; @@ -55,20 +56,20 @@ impl UError for NohupError { } } -fn failure_code() -> i32 { - if env::var("POSIXLY_CORRECT").is_ok() { +static FAILURE_CODE: LazyLock = LazyLock::new(|| { + if env::var_os("POSIXLY_CORRECT").is_some() { POSIX_NOHUP_FAILURE } else { EXIT_CANCELED } -} +}); #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uucore::clap_localization::handle_clap_result_with_exit_code( uu_app(), args, - failure_code(), + *FAILURE_CODE, )?; replace_fds()?; @@ -94,9 +95,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("nohup") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("nohup")) .about(translate!("nohup-about")) .after_help(translate!("nohup-after-help")) .override_usage(format_usage(&translate!("nohup-usage"))) @@ -136,8 +137,6 @@ fn replace_fds() -> UResult<()> { } fn find_stdout() -> UResult { - let internal_failure_code = failure_code(); - match OpenOptions::new() .create(true) .append(true) @@ -152,7 +151,7 @@ fn find_stdout() -> UResult { } Err(e1) => { let Ok(home) = env::var("HOME") else { - return Err(NohupError::OpenFailed(internal_failure_code, e1).into()); + return Err(NohupError::OpenFailed(*FAILURE_CODE, e1).into()); }; let mut homeout = PathBuf::from(home); homeout.push(NOHUP_OUT); @@ -165,13 +164,12 @@ fn find_stdout() -> UResult { ); Ok(t) } - Err(e2) => Err(NohupError::OpenFailed2( - internal_failure_code, - e1, - homeout_str.to_string(), - e2, - ) - .into()), + Err(e2) => { + Err( + NohupError::OpenFailed2(*FAILURE_CODE, e1, homeout_str.to_string(), e2) + .into(), + ) + } } } } diff --git a/src/uu/nproc/src/nproc.rs b/src/uu/nproc/src/nproc.rs index bc6a8a30007..9edf21bf613 100644 --- a/src/uu/nproc/src/nproc.rs +++ b/src/uu/nproc/src/nproc.rs @@ -86,9 +86,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("nproc") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("nproc")) .about(translate!("nproc-about")) .override_usage(format_usage(&translate!("nproc-usage"))) .infer_long_args(true) diff --git a/src/uu/numfmt/Cargo.toml b/src/uu/numfmt/Cargo.toml index e7ce4f9c951..f17737dd4c9 100644 --- a/src/uu/numfmt/Cargo.toml +++ b/src/uu/numfmt/Cargo.toml @@ -20,7 +20,12 @@ path = "src/numfmt.rs" [dependencies] clap = { workspace = true } -uucore = { workspace = true, features = ["parser", "ranges"] } +uucore = { workspace = true, features = [ + "parser", + "ranges", + "i18n-common", + "i18n-decimal", +] } thiserror = { workspace = true } fluent = { workspace = true } memchr = { workspace = true } diff --git a/src/uu/numfmt/locales/en-US.ftl b/src/uu/numfmt/locales/en-US.ftl index 4e5faf56e7f..c0e2c5722e7 100644 --- a/src/uu/numfmt/locales/en-US.ftl +++ b/src/uu/numfmt/locales/en-US.ftl @@ -42,6 +42,7 @@ numfmt-help-field = replace the numbers in these input fields; see FIELDS below numfmt-help-format = use printf style floating-point FORMAT; see FORMAT below for details numfmt-help-from = auto-scale input numbers to UNITs; see UNIT below numfmt-help-from-unit = specify the input unit size +numfmt-help-grouping = use locale-defined grouping of digits, for example 1,000,000 (which means it has no effect in the C/POSIX locale) numfmt-help-to = auto-scale output numbers to UNITs; see UNIT below numfmt-help-to-unit = the output unit size numfmt-help-padding = pad the output to N characters; positive N will right-align; negative N will left-align; padding is ignored if the output is wider than N; the default is to automatically pad if a whitespace is found @@ -57,6 +58,7 @@ numfmt-error-unsupported-unit = Unsupported unit is specified numfmt-error-invalid-unit-size = invalid unit size: { $size } numfmt-error-invalid-padding = invalid padding value { $value } numfmt-error-invalid-header = invalid header value { $value } +numfmt-error-grouping-cannot-be-combined-with-format = --grouping cannot be combined with --format numfmt-error-grouping-cannot-be-combined-with-to = grouping cannot be combined with --to numfmt-error-delimiter-must-be-single-character = the delimiter must be a single character numfmt-error-invalid-number-empty = invalid number: '' @@ -78,4 +80,6 @@ numfmt-error-unknown-invalid-mode = Unknown invalid mode: { $mode } # Debug messages numfmt-debug-no-conversion = no conversion option specified +numfmt-debug-grouping-no-effect = grouping has no effect in this locale +numfmt-debug-failed-to-convert = failed to convert some of the input numbers numfmt-debug-header-ignored = --header ignored with command-line input diff --git a/src/uu/numfmt/locales/fr-FR.ftl b/src/uu/numfmt/locales/fr-FR.ftl index 20bd91db9a9..71f0ab54063 100644 --- a/src/uu/numfmt/locales/fr-FR.ftl +++ b/src/uu/numfmt/locales/fr-FR.ftl @@ -30,17 +30,19 @@ numfmt-after-help = Options d'UNITÉ : Plusieurs champs/plages peuvent être séparés par des virgules FORMAT doit être adapté pour imprimer un argument à virgule flottante %f. - Une guillemet optionnelle (%'f) activera --grouping (si supporté par la locale actuelle). + Une guillemet optionnelle (%'f) activera --grouping (si pris en charge par la locale actuelle). Une valeur de largeur optionnelle (%10f) remplira la sortie. Un zéro optionnel (%010f) remplira le nombre de zéros. Des valeurs négatives optionnelles (%-10f) aligneront à gauche. Une précision optionnelle (%.1f) remplacera la précision déterminée par l'entrée. # Messages d'aide +numfmt-help-debug = afficher des avertissements sur les entrées invalides numfmt-help-delimiter = utiliser X au lieu d'espaces pour le délimiteur de champ numfmt-help-field = remplacer les nombres dans ces champs d'entrée ; voir FIELDS ci-dessous numfmt-help-format = utiliser le FORMAT à virgule flottante de style printf ; voir FORMAT ci-dessous pour les détails numfmt-help-from = mettre automatiquement à l'échelle les nombres d'entrée vers les UNITÉs ; voir UNIT ci-dessous numfmt-help-from-unit = spécifier la taille de l'unité d'entrée +numfmt-help-grouping = utiliser le groupement des chiffres défini par la locale, par exemple 1 000 000 (ce qui n'a aucun effet dans la locale C/POSIX) numfmt-help-to = mettre automatiquement à l'échelle les nombres de sortie vers les UNITÉs ; voir UNIT ci-dessous numfmt-help-to-unit = la taille de l'unité de sortie numfmt-help-padding = remplir la sortie à N caractères ; N positif alignera à droite ; N négatif alignera à gauche ; le remplissage est ignoré si la sortie est plus large que N ; la valeur par défaut est de remplir automatiquement si un espace est trouvé @@ -51,10 +53,11 @@ numfmt-help-invalid = définir le mode d'échec pour les entrées invalides numfmt-help-zero-terminated = le délimiteur de ligne est NUL, pas retour à la ligne # Messages d'erreur -numfmt-error-unsupported-unit = Une unité non supportée est spécifiée +numfmt-error-unsupported-unit = Une unité non prise en charge est spécifiée numfmt-error-invalid-unit-size = taille d'unité invalide : { $size } numfmt-error-invalid-padding = valeur de remplissage invalide { $value } numfmt-error-invalid-header = valeur d'en-tête invalide { $value } +numfmt-error-grouping-cannot-be-combined-with-format = --grouping ne peut pas être combiné avec --format numfmt-error-grouping-cannot-be-combined-with-to = le groupement ne peut pas être combiné avec --to numfmt-error-delimiter-must-be-single-character = le délimiteur doit être un seul caractère numfmt-error-invalid-number-empty = nombre invalide : '' @@ -63,9 +66,9 @@ numfmt-error-invalid-specific-suffix = suffixe invalide dans l'entrée { $input numfmt-error-invalid-number = nombre invalide : { $input } numfmt-error-missing-i-suffix = suffixe 'i' manquant dans l'entrée : '{ $number }{ $suffix }' (par ex. Ki/Mi/Gi) numfmt-error-rejecting-suffix = rejet du suffixe dans l'entrée : '{ $number }{ $suffix }' (considérez utiliser --from) -numfmt-error-suffix-unsupported-for-unit = Ce suffixe n'est pas supporté pour l'unité spécifiée -numfmt-error-unit-auto-not-supported-with-to = L'unité 'auto' n'est pas supportée avec les options --to -numfmt-error-number-too-big = Le nombre est trop grand et non supporté +numfmt-error-suffix-unsupported-for-unit = Ce suffixe n'est pas pris en charge pour l'unité spécifiée +numfmt-error-unit-auto-not-supported-with-to = L'unité 'auto' n'est pas prise en charge avec les options --to +numfmt-error-number-too-big = Le nombre est trop grand et non pris en charge numfmt-error-format-no-percent = le format '{ $format }' n'a pas de directive % numfmt-error-format-ends-in-percent = le format '{ $format }' se termine par % numfmt-error-invalid-format-directive = format invalide '{ $format }', la directive doit être %[0]['][-][N][.][N]f @@ -73,3 +76,9 @@ numfmt-error-invalid-format-width-overflow = format invalide '{ $format }' (déb numfmt-error-invalid-precision = précision invalide dans le format '{ $format }' numfmt-error-format-too-many-percent = le format '{ $format }' a trop de directives % numfmt-error-unknown-invalid-mode = Mode invalide inconnu : { $mode } + +# Messages de débogage +numfmt-debug-no-conversion = aucune option de conversion spécifiée +numfmt-debug-grouping-no-effect = le groupement n'a aucun effet dans cette locale +numfmt-debug-failed-to-convert = échec de conversion d'une partie des nombres en entrée +numfmt-debug-header-ignored = --header ignoré avec une entrée en ligne de commande diff --git a/src/uu/numfmt/src/format.rs b/src/uu/numfmt/src/format.rs index 57abc84588f..e1b5101f235 100644 --- a/src/uu/numfmt/src/format.rs +++ b/src/uu/numfmt/src/format.rs @@ -3,66 +3,16 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore powf +// spell-checker:ignore powf seps use uucore::display::Quotable; +use uucore::i18n::decimal::locale_grouping_separator; use uucore::translate; use crate::options::{NumfmtOptions, RoundMethod, TransformOptions}; -use crate::units::{DisplayableSuffix, IEC_BASES, RawSuffix, Result, SI_BASES, Suffix, Unit}; - -/// Iterate over a line's fields, where each field is a contiguous sequence of -/// non-whitespace, optionally prefixed with one or more characters of leading -/// whitespace. Fields are returned as tuples of `(prefix, field)`. -/// -/// # Examples: -/// -/// ``` -/// let mut fields = uu_numfmt::format::WhitespaceSplitter { s: Some(" 1234 5") }; -/// -/// assert_eq!(Some((" ", "1234")), fields.next()); -/// assert_eq!(Some((" ", "5")), fields.next()); -/// assert_eq!(None, fields.next()); -/// ``` -/// -/// Delimiters are included in the results; `prefix` will be empty only for -/// the first field of the line (including the case where the input line is -/// empty): -/// -/// ``` -/// let mut fields = uu_numfmt::format::WhitespaceSplitter { s: Some("first second") }; -/// -/// assert_eq!(Some(("", "first")), fields.next()); -/// assert_eq!(Some((" ", "second")), fields.next()); -/// -/// let mut fields = uu_numfmt::format::WhitespaceSplitter { s: Some("") }; -/// -/// assert_eq!(Some(("", "")), fields.next()); -/// ``` -pub struct WhitespaceSplitter<'a> { - pub s: Option<&'a str>, -} - -impl<'a> Iterator for WhitespaceSplitter<'a> { - type Item = (&'a str, &'a str); - - /// Yield the next field in the input string as a tuple `(prefix, field)`. - fn next(&mut self) -> Option { - let haystack = self.s?; - - let (prefix, field) = haystack.split_at( - haystack - .find(|c: char| !c.is_whitespace()) - .unwrap_or(haystack.len()), - ); - - let (field, rest) = field.split_at(field.find(char::is_whitespace).unwrap_or(field.len())); - - self.s = if rest.is_empty() { None } else { Some(rest) }; - - Some((prefix, field)) - } -} +use crate::units::{ + DisplayableSuffix, RawSuffix, Result, Suffix, Unit, iec_bases_f64, si_bases_f64, +}; fn find_numeric_beginning(s: &str) -> Option<&str> { let mut decimal_point_seen = false; @@ -70,6 +20,10 @@ fn find_numeric_beginning(s: &str) -> Option<&str> { return None; } + if s.starts_with('.') { + return Some("."); + } + for (idx, c) in s.char_indices() { if c == '-' && idx == 0 { continue; @@ -119,17 +73,63 @@ fn find_valid_number_with_suffix(s: &str, unit: Unit) -> Option<&str> { } } -fn detailed_error_message(s: &str, unit: Unit) -> Option { +/// Given a string like "5 K Field2" with unit_separator=" ", returns the length +/// of the valid prefix including the separator and suffix (e.g. "5 K" → 4). +fn valid_end_with_unit_separator( + s: &str, + valid_part: &str, + unit: Unit, + unit_separator: &str, +) -> Option { + let after_sep = s.get(valid_part.len()..)?.strip_prefix(unit_separator)?; + + let mut chars = after_sep.chars(); + let first_char = chars.next()?; + + RawSuffix::try_from(&first_char).ok()?; + + let is_iec = chars.next() == Some('i') && matches!(unit, Unit::Auto | Unit::Iec(true)); + let suffix_len = 1 + usize::from(is_iec); + + Some(valid_part.len() + unit_separator.len() + suffix_len) +} + +fn detailed_error_message(s: &str, unit: Unit, unit_separator: &str) -> Option { if s.is_empty() { return Some(translate!("numfmt-error-invalid-number-empty")); } - let valid_part = find_valid_number_with_suffix(s, unit) + let number_prefix = find_valid_number_with_suffix(s, unit) .ok_or(translate!("numfmt-error-invalid-number", "input" => s.quote())) .ok()?; + if number_prefix == "." { + return Some(translate!("numfmt-error-invalid-suffix", "input" => s.quote())); + } + + if number_prefix.ends_with('.') { + return Some(translate!("numfmt-error-invalid-number", "input" => s.quote())); + } + + // When a unit separator is in use, the valid part may extend beyond the + // contiguous number+suffix found by find_valid_number_with_suffix. + // For example "5 K Field2" with unit_separator=" " has number_prefix="5" but + // the real valid prefix is "5 K"; the trailing " Field2" is the garbage. + let valid_end = + if !unit_separator.is_empty() && number_prefix == find_numeric_beginning(s).unwrap_or("") { + valid_end_with_unit_separator(s, number_prefix, unit, unit_separator) + .unwrap_or(number_prefix.len()) + } else { + number_prefix.len() + }; + + let valid_part = &s[..valid_end]; + if valid_part != s && valid_part.parse::().is_ok() { return match s.chars().nth(valid_part.len()) { + Some('+' | '-') => { + Some(translate!("numfmt-error-invalid-suffix", "input" => s.quote())) + } Some(v) if RawSuffix::try_from(&v).is_ok() => Some( translate!("numfmt-error-rejecting-suffix", "number" => valid_part, "suffix" => s[valid_part.len()..]), ), @@ -138,15 +138,30 @@ fn detailed_error_message(s: &str, unit: Unit) -> Option { }; } - if valid_part != s && valid_part.parse::().is_err() { + if valid_part != s { + let trailing = s[valid_part.len()..].trim_start(); return Some( - translate!("numfmt-error-invalid-specific-suffix", "input" => s.quote(), "suffix" => s[valid_part.len()..].quote()), + translate!("numfmt-error-invalid-specific-suffix", "input" => s.quote(), "suffix" => trailing.quote()), ); } None } -fn parse_suffix(s: &str, unit: Unit, max_whitespace: usize) -> Result<(f64, Option)> { +fn parse_number_part(s: &str, input: &str) -> Result { + if s.ends_with('.') { + return Err(translate!("numfmt-error-invalid-number", "input" => input.quote())); + } + + s.parse::() + .map_err(|_| translate!("numfmt-error-invalid-number", "input" => input.quote())) +} + +fn parse_suffix( + s: &str, + unit: Unit, + unit_separator: &str, + explicit_unit_separator: bool, +) -> Result<(f64, Option)> { let trimmed = s.trim_end(); if trimmed.is_empty() { return Err(translate!("numfmt-error-invalid-number-empty")); @@ -160,48 +175,163 @@ fn parse_suffix(s: &str, unit: Unit, max_whitespace: usize) -> Result<(f64, Opti if with_i { iter.next_back(); } - let suffix = match iter.next_back() { - Some('K') => Some((RawSuffix::K, with_i)), - Some('k') => Some((RawSuffix::K, with_i)), - Some('M') => Some((RawSuffix::M, with_i)), - Some('G') => Some((RawSuffix::G, with_i)), - Some('T') => Some((RawSuffix::T, with_i)), - Some('P') => Some((RawSuffix::P, with_i)), - Some('E') => Some((RawSuffix::E, with_i)), - Some('Z') => Some((RawSuffix::Z, with_i)), - Some('Y') => Some((RawSuffix::Y, with_i)), - Some('R') => Some((RawSuffix::R, with_i)), - Some('Q') => Some((RawSuffix::Q, with_i)), - Some('0'..='9') if !with_i => None, - _ => { - return Err(translate!("numfmt-error-invalid-number", "input" => s.quote())); - } - }; + let last = iter.next_back(); + let suffix = last + .and_then(|c| RawSuffix::try_from(&c).ok()) + .map(|raw| (raw, with_i)); + match (suffix, last) { + (Some(_), _) => {} + (None, Some(c)) if c.is_ascii_digit() && !with_i => {} + _ => return Err(translate!("numfmt-error-invalid-number", "input" => s.quote())), + } - let suffix_len = match suffix { - None => 0, - Some((_, false)) => 1, - Some((_, true)) => 2, - }; + let suffix_len = suffix.map_or(0, |(_, with_i)| 1 + usize::from(with_i)); let number_part = &trimmed[..trimmed.len() - suffix_len]; - let number_trimmed = number_part.trim_end(); - // Validate whitespace between number and suffix if suffix.is_some() { - let whitespace = number_part.len() - number_trimmed.len(); - if whitespace > max_whitespace { - return Err(translate!("numfmt-error-invalid-suffix", "input" => s.quote())); - } + let separator_len = if explicit_unit_separator { + if number_part.ends_with(unit_separator) { + unit_separator.len() + } else if unit_separator.is_empty() { + 0 + } else { + return Err(translate!("numfmt-error-invalid-suffix", "input" => s.quote())); + } + } else { + let number_trimmed = number_part.trim_end(); + let whitespace = number_part.len() - number_trimmed.len(); + if whitespace > 1 { + return Err(translate!("numfmt-error-invalid-suffix", "input" => s.quote())); + } + whitespace + }; + + let number = parse_number_part(&number_part[..number_part.len() - separator_len], s)?; + + return Ok((number, suffix)); } - let number = number_trimmed - .parse::() - .map_err(|_| translate!("numfmt-error-invalid-number", "input" => s.quote()))?; + let number = parse_number_part(number_part, s)?; Ok((number, suffix)) } +fn apply_grouping(s: &str) -> String { + let grouping_separator = locale_grouping_separator(); + if grouping_separator.is_empty() { + return s.to_string(); + } + + let (sign, rest) = if let Some(rest) = s.strip_prefix('-') { + ("-", rest) + } else { + ("", s) + }; + let (integer, fraction) = rest.split_once('.').map_or((rest, ""), |(i, f)| (i, f)); + if integer.len() < 4 { + return s.to_string(); + } + + let sep_len = grouping_separator.len(); + let num_seps = (integer.len() - 1) / 3; + let mut grouped = String::with_capacity( + sign.len() + + integer.len() + + num_seps * sep_len + + if fraction.is_empty() { + 0 + } else { + 1 + fraction.len() + }, + ); + grouped.push_str(sign); + + let first_group = integer.len() % 3; + let first_group = if first_group == 0 { 3 } else { first_group }; + grouped.push_str(&integer[..first_group]); + for chunk in integer.as_bytes()[first_group..].chunks(3) { + grouped.push_str(grouping_separator); + // SAFETY: integer is known to be valid UTF-8 ASCII digits + grouped.push_str(std::str::from_utf8(chunk).unwrap()); + } + + if !fraction.is_empty() { + grouped.push('.'); + grouped.push_str(fraction); + } + + grouped +} + +fn split_next_field(s: &str) -> (&str, &str, &str) { + let prefix_len = s.find(|c: char| !c.is_whitespace()).unwrap_or(s.len()); + let field_end = s[prefix_len..] + .find(char::is_whitespace) + .map_or(s.len(), |i| prefix_len + i); + (&s[..prefix_len], &s[prefix_len..field_end], &s[field_end..]) +} + +/// When an explicit whitespace unit separator is set (e.g. `--unit-separator=" "`), +/// a suffix like "K" may appear as a separate whitespace-delimited field. Detect +/// this case so the caller can merge the suffix back into the preceding number field. +fn split_mergeable_suffix<'a>(s: &'a str, options: &NumfmtOptions) -> Option<(&'a str, &'a str)> { + if !options.explicit_unit_separator + || options.unit_separator.is_empty() + || !options.unit_separator.chars().all(char::is_whitespace) + { + return None; + } + + if !s.starts_with(&options.unit_separator) { + return None; + } + + let (prefix, field, _) = split_next_field(s); + if prefix != options.unit_separator { + return None; + } + + let first_char = field.chars().next()?; + RawSuffix::try_from(&first_char).ok()?; + match field.len() { + 1 => {} + 2 if field.ends_with('i') => {} + _ => return None, + } + + Some((prefix, field)) +} + +struct WhitespaceSplitter<'a, 'b> { + s: Option<&'a str>, + options: &'b NumfmtOptions, +} + +impl<'a> Iterator for WhitespaceSplitter<'a, '_> { + type Item = (&'a str, &'a str); + + fn next(&mut self) -> Option { + let haystack = self.s?; + let (prefix, field, rest) = split_next_field(haystack); + + if field.is_empty() { + self.s = None; + return Some((prefix, field)); + } + + if let Some((suffix_prefix, suffix_field)) = split_mergeable_suffix(rest, self.options) { + let merged_len = prefix.len() + field.len() + suffix_prefix.len() + suffix_field.len(); + let merged_field = &haystack[prefix.len()..merged_len]; + self.s = Some(&haystack[merged_len..]).filter(|rest| !rest.is_empty()); + return Some((prefix, merged_field)); + } + + self.s = Some(rest).filter(|rest| !rest.is_empty()); + Some((prefix, field)) + } +} + /// Returns the implicit precision of a number, which is the count of digits after the dot. For /// example, 1.23 has an implicit precision of 2. fn parse_implicit_precision(s: &str) -> usize { @@ -215,51 +345,41 @@ fn parse_implicit_precision(s: &str) -> usize { } fn remove_suffix(i: f64, s: Option, u: Unit) -> Result { - match (s, u) { - (Some((raw_suffix, false)), Unit::Auto | Unit::Si) => match raw_suffix { - RawSuffix::K => Ok(i * 1e3), - RawSuffix::M => Ok(i * 1e6), - RawSuffix::G => Ok(i * 1e9), - RawSuffix::T => Ok(i * 1e12), - RawSuffix::P => Ok(i * 1e15), - RawSuffix::E => Ok(i * 1e18), - RawSuffix::Z => Ok(i * 1e21), - RawSuffix::Y => Ok(i * 1e24), - RawSuffix::R => Ok(i * 1e27), - RawSuffix::Q => Ok(i * 1e30), - }, - (Some((raw_suffix, false)), Unit::Iec(false)) - | (Some((raw_suffix, true)), Unit::Auto | Unit::Iec(true)) => match raw_suffix { - RawSuffix::K => Ok(i * IEC_BASES[1]), - RawSuffix::M => Ok(i * IEC_BASES[2]), - RawSuffix::G => Ok(i * IEC_BASES[3]), - RawSuffix::T => Ok(i * IEC_BASES[4]), - RawSuffix::P => Ok(i * IEC_BASES[5]), - RawSuffix::E => Ok(i * IEC_BASES[6]), - RawSuffix::Z => Ok(i * IEC_BASES[7]), - RawSuffix::Y => Ok(i * IEC_BASES[8]), - RawSuffix::R => Ok(i * IEC_BASES[9]), - RawSuffix::Q => Ok(i * IEC_BASES[10]), - }, - (Some((raw_suffix, false)), Unit::Iec(true)) => Err( + let Some((raw_suffix, with_i)) = s else { + return Ok(i); + }; + let idx = raw_suffix.index() + 1; + match (with_i, u) { + (false, Unit::Auto | Unit::Si) => Ok(i * si_bases_f64()[idx]), + (false, Unit::Iec(false)) | (true, Unit::Auto | Unit::Iec(true)) => { + Ok(i * iec_bases_f64()[idx]) + } + (false, Unit::Iec(true)) => Err( translate!("numfmt-error-missing-i-suffix", "number" => i, "suffix" => format!("{raw_suffix:?}")), ), - (Some((raw_suffix, with_i)), Unit::None) => Err( + (_, Unit::None) => Err( translate!("numfmt-error-rejecting-suffix", "number" => i, "suffix" => format!("{raw_suffix:?}{}", if with_i { "i" } else { "" })), ), - (None, _) => Ok(i), - (_, _) => Err(translate!("numfmt-error-suffix-unsupported-for-unit")), + _ => Err(translate!("numfmt-error-suffix-unsupported-for-unit")), } } -fn transform_from(s: &str, opts: &TransformOptions, max_whitespace: usize) -> Result { - let (i, suffix) = parse_suffix(s, opts.from, max_whitespace) - .map_err(|original| detailed_error_message(s, opts.from).unwrap_or(original))?; +fn transform_from(s: &str, opts: &TransformOptions, options: &NumfmtOptions) -> Result { + let (i, suffix) = parse_suffix( + s, + opts.from, + &options.unit_separator, + options.explicit_unit_separator, + ) + .map_err(|original| { + detailed_error_message(s, opts.from, &options.unit_separator).unwrap_or(original) + })?; + let had_no_suffix = suffix.is_none(); let i = i * (opts.from_unit as f64); remove_suffix(i, suffix, opts.from).map(|n| { // GNU numfmt doesn't round values if no --from argument is provided by the user - if opts.from == Unit::None { + if opts.from == Unit::None || had_no_suffix { if n == -0.0 { 0.0 } else { n } } else if n < 0.0 { -n.abs().ceil() @@ -325,8 +445,8 @@ fn consider_suffix( let suffixes = [K, M, G, T, P, E, Z, Y, R, Q]; let (bases, with_i) = match u { - Unit::Si => (&SI_BASES, false), - Unit::Iec(with_i) => (&IEC_BASES, with_i), + Unit::Si => (si_bases_f64(), false), + Unit::Iec(with_i) => (iec_bases_f64(), with_i), Unit::Auto => return Err(translate!("numfmt-error-unit-auto-not-supported-with-to")), Unit::None => return Ok((n, None)), }; @@ -389,6 +509,26 @@ fn transform_to( }) } +/// Pad `s` to at least `width` characters using `fill`. +/// Right-aligns when `right_align` is true, left-aligns otherwise. +/// Unlike `format!("{:>width$}")`, this handles widths larger than 65535. +fn pad_string(s: &str, width: usize, fill: char, right_align: bool) -> String { + let len = s.len(); + if len >= width { + return s.to_string(); + } + let pad = width - len; + let mut result = String::with_capacity(width); + if right_align { + result.extend(std::iter::repeat_n(fill, pad)); + result.push_str(s); + } else { + result.push_str(s); + result.extend(std::iter::repeat_n(fill, pad)); + } + result +} + fn format_string( source: &str, options: &NumfmtOptions, @@ -402,18 +542,19 @@ fn format_string( let precision = if let Some(p) = options.format.precision { p - } else if options.transform.from == Unit::None && options.transform.to == Unit::None { + } else if options.transform.to == Unit::None + && !source_without_suffix + .chars() + .last() + .is_some_and(char::is_alphabetic) + { parse_implicit_precision(source_without_suffix) } else { 0 }; let number = transform_to( - transform_from( - source_without_suffix, - &options.transform, - options.max_whitespace, - )?, + transform_from(source_without_suffix, &options.transform, options)?, &options.transform, options.round, precision, @@ -421,9 +562,15 @@ fn format_string( )?; // bring back the suffix before applying padding + let grouped_number = if options.grouping { + apply_grouping(&number) + } else { + number + }; + let number_with_suffix = match &options.suffix { - Some(suffix) => format!("{number}{suffix}"), - None => number, + Some(suffix) => format!("{grouped_number}{suffix}"), + None => grouped_number, }; let padding = options @@ -434,16 +581,16 @@ fn format_string( let padded_number = match padding { 0 => number_with_suffix, p if p > 0 && options.format.zero_padding => { - let zero_padded = format!("{number_with_suffix:0>padding$}", padding = p as usize); + let zero_padded = pad_string(&number_with_suffix, p as usize, '0', true); match implicit_padding.unwrap_or(options.padding) { 0 => zero_padded, - p if p > 0 => format!("{zero_padded:>padding$}", padding = p as usize), - p => format!("{zero_padded: 0 => pad_string(&zero_padded, p as usize, ' ', true), + p => pad_string(&zero_padded, p.unsigned_abs(), ' ', false), } } - p if p > 0 => format!("{number_with_suffix:>padding$}", padding = p as usize), - p => format!("{number_with_suffix: 0 => pad_string(&number_with_suffix, p as usize, ' ', true), + p => pad_string(&number_with_suffix, p.unsigned_abs(), ' ', false), }; Ok(format!( @@ -489,7 +636,7 @@ fn split_bytes<'a>(input: &'a [u8], delim: &'a [u8]) -> impl Iterator( +pub fn write_formatted_with_delimiter( writer: &mut W, input: &[u8], options: &NumfmtOptions, @@ -525,13 +672,16 @@ pub fn write_formatted_with_delimiter( Ok(()) } -pub fn write_formatted_with_whitespace( +pub fn write_formatted_with_whitespace( writer: &mut W, s: &str, options: &NumfmtOptions, eol: Option, ) -> Result<()> { - for (n, (prefix, field)) in (1..).zip(WhitespaceSplitter { s: Some(s) }) { + for (n, (prefix, field)) in (1..).zip(WhitespaceSplitter { + s: Some(s), + options, + }) { let field_selected = uucore::ranges::contain(&options.fields, n); if field_selected { @@ -613,7 +763,7 @@ mod tests { #[test] fn test_parse_suffix_q_r_k() { - let result = parse_suffix("1Q", Unit::Auto, 1); + let result = parse_suffix("1Q", Unit::Auto, "", false); assert!(result.is_ok()); let (number, suffix) = result.unwrap(); assert_eq!(number, 1.0); @@ -622,7 +772,7 @@ mod tests { assert_eq!(raw_suffix as i32, RawSuffix::Q as i32); assert!(!with_i); - let result = parse_suffix("2R", Unit::Auto, 1); + let result = parse_suffix("2R", Unit::Auto, "", false); assert!(result.is_ok()); let (number, suffix) = result.unwrap(); assert_eq!(number, 2.0); @@ -631,7 +781,7 @@ mod tests { assert_eq!(raw_suffix as i32, RawSuffix::R as i32); assert!(!with_i); - let result = parse_suffix("3k", Unit::Auto, 1); + let result = parse_suffix("3k", Unit::Auto, "", false); assert!(result.is_ok()); let (number, suffix) = result.unwrap(); assert_eq!(number, 3.0); @@ -640,7 +790,7 @@ mod tests { assert_eq!(raw_suffix as i32, RawSuffix::K as i32); assert!(!with_i); - let result = parse_suffix("4Qi", Unit::Auto, 1); + let result = parse_suffix("4Qi", Unit::Auto, "", false); assert!(result.is_ok()); let (number, suffix) = result.unwrap(); assert_eq!(number, 4.0); @@ -649,7 +799,7 @@ mod tests { assert_eq!(raw_suffix as i32, RawSuffix::Q as i32); assert!(with_i); - let result = parse_suffix("5Ri", Unit::Auto, 1); + let result = parse_suffix("5Ri", Unit::Auto, "", false); assert!(result.is_ok()); let (number, suffix) = result.unwrap(); assert_eq!(number, 5.0); @@ -661,13 +811,13 @@ mod tests { #[test] fn test_parse_suffix_error_messages() { - let result = parse_suffix("foo", Unit::Auto, 1); + let result = parse_suffix("foo", Unit::Auto, "", false); assert!(result.is_err()); let error = result.unwrap_err(); assert!(error.contains("numfmt-error-invalid-number") || error.contains("invalid number")); assert!(!error.contains("invalid suffix")); - let result = parse_suffix("World", Unit::Auto, 1); + let result = parse_suffix("World", Unit::Auto, "", false); assert!(result.is_err()); let error = result.unwrap_err(); assert!(error.contains("numfmt-error-invalid-number") || error.contains("invalid number")); @@ -676,12 +826,12 @@ mod tests { #[test] fn test_detailed_error_message() { - let result = detailed_error_message("123i", Unit::Auto); + let result = detailed_error_message("123i", Unit::Auto, ""); assert!(result.is_some()); let error = result.unwrap(); assert!(error.contains("numfmt-error-invalid-suffix") || error.contains("invalid suffix")); - let result = detailed_error_message("5MF", Unit::Auto); + let result = detailed_error_message("5MF", Unit::Auto, ""); assert!(result.is_some()); let error = result.unwrap(); assert!( @@ -689,7 +839,7 @@ mod tests { || error.contains("invalid suffix") ); - let result = detailed_error_message("5KM", Unit::Auto); + let result = detailed_error_message("5KM", Unit::Auto, ""); assert!(result.is_some()); let error = result.unwrap(); assert!( @@ -710,13 +860,14 @@ mod tests { assert!(result.is_ok()); assert_eq!(result.unwrap(), 1e27); + let iec = iec_bases_f64(); let result = remove_suffix(1.0, Some((RawSuffix::Q, true)), Unit::Iec(true)); assert!(result.is_ok()); - assert_eq!(result.unwrap(), IEC_BASES[10]); + assert_eq!(result.unwrap(), iec[10]); let result = remove_suffix(1.0, Some((RawSuffix::R, true)), Unit::Iec(true)); assert!(result.is_ok()); - assert_eq!(result.unwrap(), IEC_BASES[9]); + assert_eq!(result.unwrap(), iec[9]); } #[test] @@ -814,4 +965,140 @@ mod tests { assert_eq!(raw_suffix as i32, RawSuffix::Q as i32); assert_eq!(value, 5.0); } + + #[test] + fn test_detailed_error_message_empty() { + let result = detailed_error_message("", Unit::Auto, ""); + assert!(result.is_some()); + } + + #[test] + fn test_detailed_error_message_valid_number() { + // A plain valid number should return None (no error) + assert!(detailed_error_message("123", Unit::Auto, "").is_none()); + assert!(detailed_error_message("5K", Unit::Auto, "").is_none()); + assert!(detailed_error_message("-3.5M", Unit::Auto, "").is_none()); + } + + #[test] + fn test_detailed_error_message_trailing_garbage() { + // Number with suffix followed by extra chars + let result = detailed_error_message("5Kx", Unit::Auto, "").unwrap(); + assert!( + result.contains("numfmt-error-invalid-specific-suffix") + || result.contains("invalid suffix") + ); + } + + #[test] + fn test_detailed_error_message_dot_only() { + let result = detailed_error_message(".", Unit::Auto, "").unwrap(); + assert!( + result.contains("numfmt-error-invalid-suffix") || result.contains("invalid suffix") + ); + } + + #[test] + fn test_detailed_error_message_trailing_dot() { + let result = detailed_error_message("5.", Unit::Auto, "").unwrap(); + assert!( + result.contains("numfmt-error-invalid-number") || result.contains("invalid number") + ); + } + + #[test] + fn test_detailed_error_message_unit_separator() { + // With unit separator, "5 K" is valid + assert!(detailed_error_message("5 K", Unit::Auto, " ").is_none()); + + // "5 Kx" should report trailing garbage after the suffix + let result = detailed_error_message("5 Kx", Unit::Auto, " "); + assert!(result.is_some()); + } + + #[test] + fn test_parse_number_part_valid() { + assert_eq!(parse_number_part("42", "42").unwrap(), 42.0); + assert_eq!(parse_number_part("-3.5", "-3.5").unwrap(), -3.5); + assert_eq!(parse_number_part("0", "0").unwrap(), 0.0); + } + + #[test] + fn test_parse_number_part_trailing_dot() { + assert!(parse_number_part("5.", "5.").is_err()); + } + + #[test] + fn test_parse_number_part_non_numeric() { + assert!(parse_number_part("abc", "abc").is_err()); + assert!(parse_number_part("", "").is_err()); + } + + #[test] + fn test_apply_grouping_short_numbers() { + // Numbers with fewer than 4 digits should be unchanged + assert_eq!(apply_grouping("0"), "0"); + assert_eq!(apply_grouping("999"), "999"); + assert_eq!(apply_grouping("-99"), "-99"); + } + + #[test] + fn test_apply_grouping_with_fraction() { + // Fraction part should not be grouped + let result = apply_grouping("1234.567"); + // Depending on locale, separator may or may not be present + assert!(result.contains("567")); + assert!(result.contains('.')); + } + + #[test] + fn test_apply_grouping_negative() { + let result = apply_grouping("-1234"); + assert!(result.starts_with('-')); + } + + #[test] + fn test_apply_grouping_large_numbers() { + // These tests verify grouping structure; actual separator depends on locale + let result = apply_grouping("1000000"); + // Should have separators inserted (length grows if separator is non-empty) + assert!(result.len() >= 7); + + let result = apply_grouping("1234567890"); + assert!(result.len() >= 10); + + let result = apply_grouping("-9999999999999"); + assert!(result.starts_with('-')); + assert!(result.len() >= 13); + } + + #[test] + fn test_apply_grouping_tiny_fraction() { + // Small decimal: integer part < 4 digits, so no grouping + assert_eq!(apply_grouping("0.000001"), "0.000001"); + assert_eq!(apply_grouping("1.23456789"), "1.23456789"); + } + + #[test] + fn test_apply_grouping_exactly_four_digits() { + let result = apply_grouping("1000"); + // Should be grouped (4 digits) + assert!(result.len() >= 4); + } + + #[test] + fn test_parse_number_part_large_and_tiny() { + assert_eq!( + parse_number_part("999999999999", "999999999999").unwrap(), + 999_999_999_999.0 + ); + assert_eq!( + parse_number_part("0.000000001", "0.000000001").unwrap(), + 0.000_000_001 + ); + assert_eq!( + parse_number_part("-99999999", "-99999999").unwrap(), + -99_999_999.0 + ); + } } diff --git a/src/uu/numfmt/src/numfmt.rs b/src/uu/numfmt/src/numfmt.rs index e2e86b8e1b3..775c0b4cc0e 100644 --- a/src/uu/numfmt/src/numfmt.rs +++ b/src/uu/numfmt/src/numfmt.rs @@ -2,14 +2,15 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +// spell-checker:ignore behavior use crate::errors::NumfmtError; use crate::format::{escape_line, write_formatted_with_delimiter, write_formatted_with_whitespace}; use crate::options::{ DEBUG, DELIMITER, FIELD, FIELD_DEFAULT, FORMAT, FROM, FROM_DEFAULT, FROM_UNIT, - FROM_UNIT_DEFAULT, FormatOptions, HEADER, HEADER_DEFAULT, INVALID, InvalidModes, NUMBER, - NumfmtOptions, PADDING, ROUND, RoundMethod, SUFFIX, TO, TO_DEFAULT, TO_UNIT, TO_UNIT_DEFAULT, - TransformOptions, UNIT_SEPARATOR, ZERO_TERMINATED, + FROM_UNIT_DEFAULT, FormatOptions, GROUPING, HEADER, HEADER_DEFAULT, INVALID, InvalidModes, + NUMBER, NumfmtOptions, PADDING, ROUND, RoundMethod, SUFFIX, TO, TO_DEFAULT, TO_UNIT, + TO_UNIT_DEFAULT, TransformOptions, UNIT_SEPARATOR, ZERO_TERMINATED, }; use crate::units::{Result, Unit}; use clap::{Arg, ArgAction, ArgMatches, Command, builder::ValueParser, parser::ValueSource}; @@ -17,10 +18,10 @@ use std::ffi::OsString; use std::io::{BufRead, Write as _, stderr}; use std::str::FromStr; -use units::{IEC_BASES, SI_BASES}; use uucore::display::Quotable; use uucore::error::UResult; - +use uucore::i18n::decimal::locale_grouping_separator; +use uucore::parser::parse_size::{IEC_BASES, SI_BASES}; use uucore::parser::shortcut_value_parser::ShortcutValueParser; use uucore::ranges::Range; use uucore::{format_usage, os_str_as_bytes, show, translate}; @@ -30,20 +31,89 @@ pub mod format; pub mod options; mod units; -fn handle_args<'a>(args: impl Iterator, options: &NumfmtOptions) -> UResult<()> { +/// Format a single line and write it, handling `--invalid` error modes. +/// +/// Returns `true` if the line contained invalid input (only possible in +/// non-abort modes). +fn format_and_write( + writer: &mut W, + input_line: &[u8], + options: &NumfmtOptions, + eol: Option, +) -> UResult { + // GNU truncates at the first embedded null byte. + let line = match memchr::memchr(b'\0', input_line) { + Some(i) => &input_line[..i], + None => input_line, + }; + + // In non-abort modes we buffer the formatted output so that on error we + // can emit the original line instead. + let buffer_output = !matches!(options.invalid, InvalidModes::Abort); + let mut buf = Vec::new(); + let dest: &mut dyn std::io::Write = if buffer_output { &mut buf } else { writer }; + + let result = if options.delimiter.is_some() { + write_formatted_with_delimiter(dest, line, options, eol) + } else { + match std::str::from_utf8(line) { + Ok(s) => write_formatted_with_whitespace(dest, s, options, eol), + Err(_) => Err(translate!( + "numfmt-error-invalid-number", + "input" => escape_line(line).quote() + )), + } + }; + + if let Err(msg) = result { + match options.invalid { + InvalidModes::Abort => { + return Err(Box::new(NumfmtError::FormattingError(msg))); + } + InvalidModes::Fail => { + show!(NumfmtError::FormattingError(msg)); + } + InvalidModes::Warn => { + let _ = writeln!(stderr(), "numfmt: {msg}"); + } + InvalidModes::Ignore => {} + } + // On error, echo the original line unchanged. + writer.write_all(input_line)?; + if let Some(eol) = eol { + writer.write_all(&[eol])?; + } + return Ok(true); + } + + if buffer_output { + writer.write_all(&buf)?; + } + Ok(false) +} + +/// Process command-line number arguments. +/// +/// Returns `true` if any line contained invalid input. +fn handle_args<'a>(args: impl Iterator, options: &NumfmtOptions) -> UResult { let mut stdout = std::io::stdout().lock(); let terminator = if options.zero_terminated { 0u8 } else { b'\n' }; + let mut saw_invalid = false; for l in args { - write_line(&mut stdout, l, options, Some(terminator))?; + saw_invalid |= format_and_write(&mut stdout, l, options, Some(terminator))?; } - Ok(()) + Ok(saw_invalid) } -fn handle_buffer(mut input: R, options: &NumfmtOptions) -> UResult<()> { +/// Process lines read from stdin. +/// +/// Returns `true` if any line contained invalid input. +fn handle_buffer(mut input: R, options: &NumfmtOptions) -> UResult { let terminator = if options.zero_terminated { 0u8 } else { b'\n' }; let mut stdout = std::io::stdout().lock(); let mut buf = Vec::new(); - let mut idx = 0; + let mut line_idx = 0; + let mut saw_invalid = false; loop { buf.clear(); @@ -60,70 +130,24 @@ fn handle_buffer(mut input: R, options: &NumfmtOptions) -> UResult<( } else { &buf[..] }; - - // Emit the terminator only if the input line had one. - // i.e. if the last line of the input does not end with a newline, we should not add one. + // Emit the terminator only when the input line had one (preserve + // missing final newline). let eol = has_terminator.then_some(terminator); - if idx < options.header { + if line_idx < options.header { + // Pass header lines through unchanged. stdout.write_all(line)?; if let Some(t) = eol { stdout.write_all(&[t])?; } } else { - write_line(&mut stdout, line, options, eol)?; + saw_invalid |= format_and_write(&mut stdout, line, options, eol)?; } - idx += 1; + line_idx += 1; } - Ok(()) -} - -fn write_line( - writer: &mut W, - input_line: &[u8], - options: &NumfmtOptions, - eol: Option, -) -> UResult<()> { - // Read lines only up to null byte (as GNU does) - let line = match memchr::memchr(b'\0', input_line) { - Some(i) => &input_line[..i], - None => input_line, - }; - let handled_line = if options.delimiter.is_some() { - write_formatted_with_delimiter(writer, line, options, eol) - } else { - // Whitespace mode requires valid UTF-8 - match std::str::from_utf8(line) { - Ok(s) => write_formatted_with_whitespace(writer, s, options, eol), - Err(_) => { - Err(translate!("numfmt-error-invalid-number", "input" => escape_line(line).quote())) - } - } - }; - - if let Err(error_message) = handled_line { - match options.invalid { - InvalidModes::Abort => { - return Err(Box::new(NumfmtError::FormattingError(error_message))); - } - InvalidModes::Fail => { - show!(NumfmtError::FormattingError(error_message)); - } - InvalidModes::Warn => { - let _ = writeln!(stderr(), "numfmt: {error_message}"); - } - InvalidModes::Ignore => {} - } - writer.write_all(input_line)?; - - if let Some(eol) = eol { - writer.write_all(&[eol])?; - } - } - - Ok(()) + Ok(saw_invalid) } fn parse_unit(s: &str) -> Result { @@ -140,15 +164,16 @@ fn parse_unit(s: &str) -> Result { /// Parses a unit size. Suffixes are turned into their integer representations. For example, 'K' /// will return `Ok(1000)`, and '2K' will return `Ok(2000)`. fn parse_unit_size(s: &str) -> Result { - let number: String = s.chars().take_while(char::is_ascii_digit).collect(); - let suffix = &s[number.len()..]; + let split = s.find(|c: char| !c.is_ascii_digit()).unwrap_or(s.len()); + let (number, suffix) = s.split_at(split); - if number.is_empty() || "0".repeat(number.len()) != number { + // Reject all-zero numeric parts like "0" or "00K". + let all_zero = !number.is_empty() && number.bytes().all(|b| b == b'0'); + if !all_zero { if let Some(multiplier) = parse_unit_size_suffix(suffix) { if number.is_empty() { return Ok(multiplier); } - if let Ok(n) = number.parse::() { return Ok(n * multiplier); } @@ -169,20 +194,15 @@ fn parse_unit_size_suffix(s: &str) -> Option { return Some(1); } - let suffix = s.chars().next().unwrap(); - - if let Some(i) = ['K', 'M', 'G', 'T', 'P', 'E'] + let i = ['K', 'M', 'G', 'T', 'P', 'E'] .iter() - .position(|&ch| ch == suffix) - { - return match s.len() { - 1 => Some(SI_BASES[i + 1] as usize), - 2 if s.ends_with('i') => Some(IEC_BASES[i + 1] as usize), - _ => None, - }; - } + .position(|&ch| s.starts_with(ch))?; - None + match s.len() { + 1 => Some(SI_BASES[i + 1] as usize), + 2 if s.ends_with('i') => Some(IEC_BASES[i + 1] as usize), + _ => None, + } } /// Parse delimiter argument, ensuring it's a single character. @@ -252,12 +272,21 @@ fn parse_options(args: &ArgMatches) -> Result { Range::from_list(fields)? }; + let grouping = args.get_flag(GROUPING); let format = match args.get_one::(FORMAT) { Some(s) => s.parse()?, None => FormatOptions::default(), }; - if format.grouping && to != Unit::None { + if grouping && args.contains_id(FORMAT) { + return Err(translate!( + "numfmt-error-grouping-cannot-be-combined-with-format" + )); + } + + let grouping = grouping || format.grouping; + + if grouping && to != Unit::None { return Err(translate!( "numfmt-error-grouping-cannot-be-combined-with-to" )); @@ -285,14 +314,8 @@ fn parse_options(args: &ArgMatches) -> Result { .cloned() .unwrap_or_default(); - // Max whitespace between number and suffix: length of separator if provided, default one - let max_whitespace = if args.contains_id(UNIT_SEPARATOR) - && args.value_source(UNIT_SEPARATOR) == Some(ValueSource::CommandLine) - { - unit_separator.len() - } else { - 1 - }; + let explicit_unit_separator = args.contains_id(UNIT_SEPARATOR) + && args.value_source(UNIT_SEPARATOR) == Some(ValueSource::CommandLine); let invalid = InvalidModes::from_str(args.get_one::(INVALID).unwrap()).unwrap(); @@ -309,7 +332,8 @@ fn parse_options(args: &ArgMatches) -> Result { round, suffix, unit_separator, - max_whitespace, + grouping, + explicit_unit_separator, format, invalid, zero_terminated, @@ -327,10 +351,15 @@ fn print_debug_warnings(options: &NumfmtOptions, matches: &ArgMatches) { if options.transform.from == Unit::None && options.transform.to == Unit::None && options.padding == 0 + && !options.grouping { print_warning("numfmt-debug-no-conversion"); } + if options.grouping && locale_grouping_separator().is_empty() { + print_warning("numfmt-debug-grouping-no-effect"); + } + // Warn if --header is used with command-line input if options.header > 0 && matches.get_many::(NUMBER).is_some() { print_warning("numfmt-debug-header-ignored"); @@ -340,7 +369,6 @@ fn print_debug_warnings(options: &NumfmtOptions, matches: &ArgMatches) { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?; - let options = parse_options(&matches).map_err(NumfmtError::IllegalArgument)?; if options.debug { @@ -355,16 +383,26 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { handle_args(byte_args.into_iter(), &options) } else { let stdin = std::io::stdin(); - let mut locked_stdin = stdin.lock(); - handle_buffer(&mut locked_stdin, &options) + handle_buffer(stdin.lock(), &options) }; match result { Err(e) => { - std::io::stdout().flush().expect("error flushing stdout"); + // Flush stdout before returning the error so any partial output is + // visible (matches GNU behavior). + let _ = std::io::stdout().flush(); Err(e) } - _ => Ok(()), + Ok(saw_invalid) => { + if options.debug && saw_invalid { + let _ = writeln!( + stderr(), + "numfmt: {}", + translate!("numfmt-debug-failed-to-convert") + ); + } + Ok(()) + } } } @@ -383,6 +421,12 @@ pub fn uu_app() -> Command { .help(translate!("numfmt-help-debug")) .action(ArgAction::SetTrue), ) + .arg( + Arg::new(GROUPING) + .long(GROUPING) + .help(translate!("numfmt-help-grouping")) + .action(ArgAction::SetTrue), + ) .arg( Arg::new(DELIMITER) .short('d') @@ -530,7 +574,8 @@ mod tests { round: RoundMethod::Nearest, suffix: None, unit_separator: String::new(), - max_whitespace: 1, + grouping: false, + explicit_unit_separator: false, format: FormatOptions::default(), invalid: InvalidModes::Abort, zero_terminated: false, diff --git a/src/uu/numfmt/src/options.rs b/src/uu/numfmt/src/options.rs index f6db4ff9f89..ced2d4b6493 100644 --- a/src/uu/numfmt/src/options.rs +++ b/src/uu/numfmt/src/options.rs @@ -17,6 +17,7 @@ pub const FROM: &str = "from"; pub const FROM_DEFAULT: &str = "none"; pub const FROM_UNIT: &str = "from-unit"; pub const FROM_UNIT_DEFAULT: &str = "1"; +pub const GROUPING: &str = "grouping"; pub const HEADER: &str = "header"; pub const HEADER_DEFAULT: &str = "1"; pub const INVALID: &str = "invalid"; @@ -55,7 +56,8 @@ pub struct NumfmtOptions { pub round: RoundMethod, pub suffix: Option, pub unit_separator: String, - pub max_whitespace: usize, + pub grouping: bool, + pub explicit_unit_separator: bool, pub format: FormatOptions, pub invalid: InvalidModes, pub zero_terminated: bool, diff --git a/src/uu/numfmt/src/units.rs b/src/uu/numfmt/src/units.rs index 4343175f396..45cacffb703 100644 --- a/src/uu/numfmt/src/units.rs +++ b/src/uu/numfmt/src/units.rs @@ -3,22 +3,18 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. use std::fmt; +use uucore::parser::parse_size::{IEC_BASES, SI_BASES}; -pub const SI_BASES: [f64; 11] = [1., 1e3, 1e6, 1e9, 1e12, 1e15, 1e18, 1e21, 1e24, 1e27, 1e30]; +/// `f64` view of [`uucore::parser::parse_size::SI_BASES`] for numfmt's +/// floating-point math paths. +pub fn si_bases_f64() -> [f64; 11] { + SI_BASES.map(|b| b as f64) +} -pub const IEC_BASES: [f64; 11] = [ - 1., - 1_024., - 1_048_576., - 1_073_741_824., - 1_099_511_627_776., - 1_125_899_906_842_624., - 1_152_921_504_606_846_976., - 1_180_591_620_717_411_303_424., - 1_208_925_819_614_629_174_706_176., - 1_237_940_039_285_380_274_899_124_224., - 1_267_650_600_228_229_401_496_703_205_376., -]; +/// `f64` view of [`uucore::parser::parse_size::IEC_BASES`]. +pub fn iec_bases_f64() -> [f64; 11] { + IEC_BASES.map(|b| b as f64) +} pub type WithI = bool; @@ -33,8 +29,9 @@ pub enum Unit { pub type Result = std::result::Result; #[derive(Clone, Copy, Debug)] +#[repr(usize)] pub enum RawSuffix { - K, + K = 0, M, G, T, @@ -46,6 +43,15 @@ pub enum RawSuffix { Q, } +impl RawSuffix { + /// Index of this suffix in the base arrays, minus one. + /// `K` is 0, `M` is 1, ..., `Q` is 9. The associated base is + /// `BASES[self.index() + 1]`. + pub fn index(self) -> usize { + self as usize + } +} + impl TryFrom<&char> for RawSuffix { type Error = String; @@ -70,25 +76,20 @@ pub type Suffix = (RawSuffix, WithI); pub struct DisplayableSuffix(pub Suffix, pub Unit); +/// Upper-case characters for each [`RawSuffix`], indexed by [`RawSuffix::index`]. +const SUFFIX_CHARS: [char; 10] = ['K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y', 'R', 'Q']; + impl fmt::Display for DisplayableSuffix { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let Self((ref raw_suffix, ref with_i), unit) = *self; - match (raw_suffix, unit) { - (RawSuffix::K, Unit::Si) => write!(f, "k"), - (RawSuffix::K, _) => write!(f, "K"), - (RawSuffix::M, _) => write!(f, "M"), - (RawSuffix::G, _) => write!(f, "G"), - (RawSuffix::T, _) => write!(f, "T"), - (RawSuffix::P, _) => write!(f, "P"), - (RawSuffix::E, _) => write!(f, "E"), - (RawSuffix::Z, _) => write!(f, "Z"), - (RawSuffix::Y, _) => write!(f, "Y"), - (RawSuffix::R, _) => write!(f, "R"), - (RawSuffix::Q, _) => write!(f, "Q"), + let Self((raw_suffix, with_i), unit) = *self; + let ch = match (raw_suffix, unit) { + (RawSuffix::K, Unit::Si) => 'k', + _ => SUFFIX_CHARS[raw_suffix.index()], + }; + write!(f, "{ch}")?; + if with_i { + write!(f, "i")?; } - .and_then(|()| match with_i { - true => write!(f, "i"), - false => Ok(()), - }) + Ok(()) } } diff --git a/src/uu/od/src/od.rs b/src/uu/od/src/od.rs index 67992d76955..1a16b9276dd 100644 --- a/src/uu/od/src/od.rs +++ b/src/uu/od/src/od.rs @@ -290,9 +290,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("od") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("od")) .about(translate!("od-about")) .override_usage(format_usage(&translate!("od-usage"))) .after_help(translate!("od-after-help")) diff --git a/src/uu/od/src/parse_formats.rs b/src/uu/od/src/parse_formats.rs index 1103b77cac3..e3312b87603 100644 --- a/src/uu/od/src/parse_formats.rs +++ b/src/uu/od/src/parse_formats.rs @@ -86,7 +86,8 @@ fn od_format_type(type_char: FormatType, byte_size: u8) -> Option Some(FORMAT_ITEM_HEX64), (FormatType::Float, 2) => Some(FORMAT_ITEM_F16), - (FormatType::Float, 0 | 4) => Some(FORMAT_ITEM_F32), + (FormatType::Float, 4) => Some(FORMAT_ITEM_F32), + (FormatType::Float, 0) => Some(FORMAT_ITEM_F64), (FormatType::Float, 8) => Some(FORMAT_ITEM_F64), (FormatType::Float, 16) => Some(FORMAT_ITEM_LONG_DOUBLE), @@ -521,7 +522,7 @@ fn test_long_format_x_default() { fn test_long_format_f_default() { assert_eq!( parse_format_flags_str(&["od", "--format=f"]).unwrap(), - vec![FORMAT_ITEM_F32] + vec![FORMAT_ITEM_F64] ); } @@ -587,7 +588,7 @@ fn test_mixed_formats() { ParsedFormatterItemInfo::new(FORMAT_ITEM_HEX8, false), // tx1 ParsedFormatterItemInfo::new(FORMAT_ITEM_DEC16U, false), // tu2 ParsedFormatterItemInfo::new(FORMAT_ITEM_C, false), // tc - ParsedFormatterItemInfo::new(FORMAT_ITEM_F32, false), // tf + ParsedFormatterItemInfo::new(FORMAT_ITEM_F64, false), // tf ParsedFormatterItemInfo::new(FORMAT_ITEM_HEX16, false), // x ] ); diff --git a/src/uu/paste/src/paste.rs b/src/uu/paste/src/paste.rs index 3d3fc3ffdab..95638c30bb4 100644 --- a/src/uu/paste/src/paste.rs +++ b/src/uu/paste/src/paste.rs @@ -42,9 +42,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("paste") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("paste")) .about(translate!("paste-about")) .override_usage(format_usage(&translate!("paste-usage"))) .infer_long_args(true) diff --git a/src/uu/pathchk/src/pathchk.rs b/src/uu/pathchk/src/pathchk.rs index 8ba3f4d37ac..0f015c8614f 100644 --- a/src/uu/pathchk/src/pathchk.rs +++ b/src/uu/pathchk/src/pathchk.rs @@ -33,6 +33,20 @@ mod options { const POSIX_PATH_MAX: usize = 256; const POSIX_NAME_MAX: usize = 14; +#[cfg(all(unix, not(target_os = "redox")))] +const PATH_MAX: usize = libc::PATH_MAX as usize; +#[cfg(all(unix, not(target_os = "redox")))] +const FILENAME_MAX: usize = libc::FILENAME_MAX as usize; +#[cfg(target_os = "redox")] +const PATH_MAX: usize = 4096; +#[cfg(target_os = "redox")] +const FILENAME_MAX: usize = 255; +// for Windows. But don't deny wasm +#[cfg(not(unix))] +const PATH_MAX: usize = 260; +#[cfg(not(unix))] +const FILENAME_MAX: usize = 255; + #[uucore::main(no_signals)] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?; @@ -81,9 +95,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("paste") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("paste")) .about(translate!("pathchk-about")) .override_usage(format_usage(&translate!("pathchk-usage"))) .infer_long_args(true) @@ -193,11 +207,11 @@ fn check_default(path: &[String]) -> bool { let joined_path = path.join("/"); let total_len = joined_path.len(); // path length - if total_len > libc::PATH_MAX as usize { + if total_len > PATH_MAX { writeln!( std::io::stderr(), "{}", - translate!("pathchk-error-path-length-exceeded", "limit" => libc::PATH_MAX, "length" => total_len, "path" => joined_path.quote()) + translate!("pathchk-error-path-length-exceeded", "limit" => PATH_MAX, "length" => total_len, "path" => joined_path.quote()) ); return false; } @@ -219,11 +233,11 @@ fn check_default(path: &[String]) -> bool { // components: length for p in path { let component_len = p.len(); - if component_len > libc::FILENAME_MAX as usize { + if component_len > FILENAME_MAX { writeln!( std::io::stderr(), "{}", - translate!("pathchk-error-name-length-exceeded", "limit" => libc::FILENAME_MAX, "length" => component_len, "component" => p.quote()) + translate!("pathchk-error-name-length-exceeded", "limit" => FILENAME_MAX, "length" => component_len, "component" => p.quote()) ); return false; } diff --git a/src/uu/pinky/locales/fr-FR.ftl b/src/uu/pinky/locales/fr-FR.ftl index ee37681c940..81a0b3f3ce8 100644 --- a/src/uu/pinky/locales/fr-FR.ftl +++ b/src/uu/pinky/locales/fr-FR.ftl @@ -39,4 +39,4 @@ pinky-project-label = Projet : pinky-plan-label = Plan # Messages de statut -pinky-unsupported-openbsd = commande non supportée sur OpenBSD +pinky-unsupported-openbsd = commande non prise en charge sur OpenBSD diff --git a/src/uu/pinky/src/pinky.rs b/src/uu/pinky/src/pinky.rs index 3ae07370a63..a0779799396 100644 --- a/src/uu/pinky/src/pinky.rs +++ b/src/uu/pinky/src/pinky.rs @@ -35,7 +35,7 @@ pub fn uu_app() -> Command { #[cfg(target_env = "musl")] let about = translate!("pinky-about") + &translate!("pinky-about-musl-warning"); - let cmd = Command::new(uucore::util_name()) + let cmd = Command::new("pinky") .version(uucore::crate_version!()) .about(about) .override_usage(format_usage(&translate!("pinky-usage"))) diff --git a/src/uu/pr/locales/en-US.ftl b/src/uu/pr/locales/en-US.ftl index 59af328dcb9..946dea2cb51 100644 --- a/src/uu/pr/locales/en-US.ftl +++ b/src/uu/pr/locales/en-US.ftl @@ -16,14 +16,14 @@ pr-help-header = pr-help-date-format = Use 'date'-style FORMAT for the header date. pr-help-double-space = - Produce output that is double spaced. An extra - character is output following every found in the input. + Produce output that is double spaced. An extra `` + character is output following every `` found in the input. pr-help-number-lines = Provide width digit line numbering. The default for width, if not specified, is 5. The number occupies the first width column positions of each text column or each line of -m output. If char (any non-digit character) is given, it is appended to the line number - to separate it from whatever follows. The default for char is a . + to separate it from whatever follows. The default for char is a ``. Line numbers longer than width columns are truncated. pr-help-first-line-number = start counting with NUMBER at 1st line of first page printed pr-help-omit-header = @@ -41,8 +41,8 @@ pr-help-page-length = option were in effect. pr-help-no-file-warnings = omit warning when a file cannot be opened pr-help-form-feed = - Use a for new pages, instead of the default behavior that - uses a sequence of s. + Use a `` for new pages, instead of the default behavior that + uses a sequence of ``s. pr-help-column-width = Set the width of the line to width column positions for multiple text-column output only. If the -w option is not specified and the -s option @@ -67,10 +67,10 @@ pr-help-column = When used with -t, use the minimum number of lines to write the output. pr-help-column-char-separator = Separate text columns by the single character char instead of by the - appropriate number of s (default for char is the character). + appropriate number of ``s (default for char is the `` character). pr-help-column-string-separator = separate columns by STRING, - without -S: Default separator with -J and + without -S: Default separator `` with -J and `` otherwise (same as -S\" \"), no effect on column options pr-help-merge = Merge files. Standard output shall be formatted so the pr utility @@ -78,7 +78,7 @@ pr-help-merge = into text columns of equal fixed widths, in terms of the number of column positions. Implementations shall support merging of at least nine file operands. pr-help-indent = - Each line of output shall be preceded by offset s. If the -o + Each line of output shall be preceded by offset ``s. If the -o option is not specified, the default offset shall be zero. The space taken is in addition to the output line width (see the -w option below). pr-help-join-lines = diff --git a/src/uu/pr/locales/fr-FR.ftl b/src/uu/pr/locales/fr-FR.ftl index 9d9a23407f8..e9c11214f44 100644 --- a/src/uu/pr/locales/fr-FR.ftl +++ b/src/uu/pr/locales/fr-FR.ftl @@ -16,15 +16,15 @@ pr-help-header = pr-help-date-format = Utiliser le FORMAT de style 'date' pour la date dans la ligne d'en-tête. pr-help-double-space = - Produire une sortie avec double espacement. Un caractère - supplémentaire est affiché après chaque trouvé dans l'entrée. + Produire une sortie avec double espacement. Un caractère `` + supplémentaire est affiché après chaque `` trouvé dans l'entrée. pr-help-number-lines = Fournir une numérotation de ligne avec largeur de chiffres. La valeur par défaut pour la largeur, si non spécifiée, est 5. Le numéro occupe les premières largeur positions de colonne de chaque colonne de texte ou de chaque ligne de sortie -m. Si char (tout caractère non numérique) est donné, il est ajouté au numéro de ligne pour le séparer de ce qui suit. La valeur par - défaut pour char est une . Les numéros de ligne plus longs + défaut pour char est une ``. Les numéros de ligne plus longs que largeur colonnes sont tronqués. pr-help-first-line-number = commencer le comptage avec NUMÉRO à la 1ère ligne de la première page imprimée pr-help-omit-header = @@ -39,8 +39,8 @@ pr-help-page-length = était en vigueur. pr-help-no-file-warnings = omettre l'avertissement lorsqu'un fichier ne peut pas être ouvert pr-help-form-feed = - Utiliser un pour les nouvelles pages, au lieu du comportement par défaut - qui utilise une séquence de . + Utiliser un `` pour les nouvelles pages, au lieu du comportement par défaut + qui utilise une séquence de ``. pr-help-column-width = Définir la largeur de la ligne à largeur positions de colonne pour la sortie multi-colonnes de texte seulement. Si l'option -w n'est pas spécifiée et @@ -66,10 +66,10 @@ pr-help-column = utiliser le nombre minimum de lignes pour écrire la sortie. pr-help-column-char-separator = Séparer les colonnes de texte par le caractère unique char au lieu du nombre - approprié d' (par défaut pour char est le caractère de ). + approprié d'`` (par défaut pour char est le caractère de ``). pr-help-column-string-separator = séparer les colonnes par CHAÎNE, - sans -S : Séparateur par défaut avec -J et + sans -S : Séparateur par défaut `` avec -J et `` sinon (même que -S\" \"), aucun effet sur les options de colonne pr-help-merge = Fusionner les fichiers. La sortie standard sera formatée pour que l'utilitaire pr @@ -77,7 +77,7 @@ pr-help-merge = dans des colonnes de texte de largeurs fixes égales, en termes du nombre de positions de colonne. Les implémentations doivent supporter la fusion d'au moins neuf opérandes de fichier. pr-help-indent = - Chaque ligne de sortie sera précédée par décalage . Si l'option -o + Chaque ligne de sortie sera précédée par décalage ``. Si l'option -o n'est pas spécifiée, le décalage par défaut sera zéro. L'espace pris est en plus de la largeur de ligne de sortie (voir l'option -w ci-dessous). pr-help-join-lines = @@ -95,7 +95,7 @@ pr-try-help-message = Essayez 'pr --help' pour plus d'informations. pr-error-reading-input = pr : La lecture depuis l'entrée {$file} a donné une erreur pr-error-unknown-filetype = pr : {$file} : type de fichier inconnu pr-error-is-directory = pr : {$file} : Est un répertoire -pr-error-socket-not-supported = pr : impossible d'ouvrir {$file}, Opération non supportée sur socket +pr-error-socket-not-supported = pr : impossible d'ouvrir {$file}, Opération non prise en charge sur socket pr-error-no-such-file = pr : impossible d'ouvrir {$file}, Aucun fichier ou répertoire de ce type pr-error-column-merge-conflict = impossible de spécifier le nombre de colonnes lors de l'impression en parallèle pr-error-across-merge-conflict = impossible de spécifier à la fois l'impression transversale et l'impression en parallèle diff --git a/src/uu/pr/src/pr.rs b/src/uu/pr/src/pr.rs index 6048a5978f9..d74c65cc592 100644 --- a/src/uu/pr/src/pr.rs +++ b/src/uu/pr/src/pr.rs @@ -192,9 +192,9 @@ enum PrError { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("pr") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("pr")) .about(translate!("pr-about")) .after_help(translate!("pr-after-help")) .override_usage(format_usage(&translate!("pr-usage"))) diff --git a/src/uu/printenv/src/printenv.rs b/src/uu/printenv/src/printenv.rs index e9f09f1d6e1..0005577c159 100644 --- a/src/uu/printenv/src/printenv.rs +++ b/src/uu/printenv/src/printenv.rs @@ -53,7 +53,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - let cmd = Command::new(uucore::util_name()) + let cmd = Command::new("printenv") .version(uucore::crate_version!()) .about(translate!("printenv-about")) .override_usage(format_usage(&translate!("printenv-usage"))) diff --git a/src/uu/printf/src/printf.rs b/src/uu/printf/src/printf.rs index 69be56c911d..4cf58bbeb5b 100644 --- a/src/uu/printf/src/printf.rs +++ b/src/uu/printf/src/printf.rs @@ -82,7 +82,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("printf") .allow_hyphen_values(true) .version(uucore::crate_version!()) .help_template(uucore::localized_help_template(uucore::util_name())) diff --git a/src/uu/ptx/src/ptx.rs b/src/uu/ptx/src/ptx.rs index 1811a29bdc0..64a5e2bacbe 100644 --- a/src/uu/ptx/src/ptx.rs +++ b/src/uu/ptx/src/ptx.rs @@ -953,10 +953,10 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("ptx") .about(translate!("ptx-about")) .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("ptx")) .override_usage(format_usage(&translate!("ptx-usage"))) .infer_long_args(true) .arg( diff --git a/src/uu/pwd/src/pwd.rs b/src/uu/pwd/src/pwd.rs index b1670b06d0a..193af4ccc5f 100644 --- a/src/uu/pwd/src/pwd.rs +++ b/src/uu/pwd/src/pwd.rs @@ -139,9 +139,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("pwd") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("pwd")) .about(translate!("pwd-about")) .override_usage(format_usage(&translate!("pwd-usage"))) .infer_long_args(true) diff --git a/src/uu/readlink/src/readlink.rs b/src/uu/readlink/src/readlink.rs index 4fc8eaa0e9c..89499cf388f 100644 --- a/src/uu/readlink/src/readlink.rs +++ b/src/uu/readlink/src/readlink.rs @@ -112,9 +112,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("readlink") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("readlink")) .about(translate!("readlink-about")) .override_usage(format_usage(&translate!("readlink-usage"))) .infer_long_args(true) diff --git a/src/uu/realpath/src/realpath.rs b/src/uu/realpath/src/realpath.rs index 860e1361ec8..f8551e7fc55 100644 --- a/src/uu/realpath/src/realpath.rs +++ b/src/uu/realpath/src/realpath.rs @@ -126,9 +126,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("realpath") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("realpath")) .about(translate!("realpath-about")) .override_usage(format_usage(&translate!("realpath-usage"))) .infer_long_args(true) diff --git a/src/uu/rm/locales/en-US.ftl b/src/uu/rm/locales/en-US.ftl index 8a9a3513eaa..2b7a570fd42 100644 --- a/src/uu/rm/locales/en-US.ftl +++ b/src/uu/rm/locales/en-US.ftl @@ -44,6 +44,7 @@ rm-error-dangerous-recursive-operation-same-as-root = it is dangerous to operate rm-error-use-no-preserve-root = use --no-preserve-root to override this failsafe rm-error-refusing-to-remove-directory = refusing to remove '.' or '..' directory: skipping {$path} rm-error-cannot-remove = cannot remove {$file} +rm-error-traversal-failed = traversal failed: {$path} rm-error-may-not-abbreviate-no-preserve-root = you may not abbreviate the --no-preserve-root option # Verbose messages diff --git a/src/uu/rm/locales/fr-FR.ftl b/src/uu/rm/locales/fr-FR.ftl index 6052e2f8964..abbe7b8eadf 100644 --- a/src/uu/rm/locales/fr-FR.ftl +++ b/src/uu/rm/locales/fr-FR.ftl @@ -28,7 +28,7 @@ rm-help-preserve-root = ne pas supprimer '/' (par défaut) rm-help-recursive = supprimer les répertoires et leur contenu récursivement rm-help-dir = supprimer les répertoires vides rm-help-verbose = expliquer ce qui est fait -rm-help-progress = afficher une barre de progression. Note : cette fonctionnalité n'est pas supportée par GNU coreutils. +rm-help-progress = afficher une barre de progression. Note : cette fonctionnalité n'est pas prise en charge par GNU coreutils. # Messages de progression rm-progress-removing = Suppression @@ -44,6 +44,7 @@ rm-error-dangerous-recursive-operation-same-as-root = il est dangereux d'opérer rm-error-use-no-preserve-root = utilisez --no-preserve-root pour outrepasser cette protection rm-error-refusing-to-remove-directory = refus de supprimer le répertoire '.' ou '..' : ignorer {$path} rm-error-cannot-remove = impossible de supprimer {$file} +rm-error-traversal-failed = échec du parcours : {$path} rm-error-may-not-abbreviate-no-preserve-root = Vous ne pouvez pas abréger l'option --no-preserve-root # Messages verbeux diff --git a/src/uu/rm/src/platform/unix.rs b/src/uu/rm/src/platform/unix.rs index d2bcf6e2f46..08ae94fdd23 100644 --- a/src/uu/rm/src/platform/unix.rs +++ b/src/uu/rm/src/platform/unix.rs @@ -349,7 +349,11 @@ pub fn safe_remove_dir_recursive_impl(path: &Path, dir_fd: &DirFd, options: &Opt return !options.force; } Err(e) => { - return handle_error_with_force(e, path, options); + let e = e.map_err_context( + || translate!("rm-error-traversal-failed", "path" => path.display().to_string()), + ); + show_error!("{e}"); + return true; } }; diff --git a/src/uu/rm/src/rm.rs b/src/uu/rm/src/rm.rs index b1c7d1dedb8..5f2b981ea2b 100644 --- a/src/uu/rm/src/rm.rs +++ b/src/uu/rm/src/rm.rs @@ -294,7 +294,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("rm") .version(uucore::crate_version!()) .about(translate!("rm-about")) .help_template(uucore::localized_help_template(uucore::util_name())) diff --git a/src/uu/rmdir/src/rmdir.rs b/src/uu/rmdir/src/rmdir.rs index e0c9f73bcec..76fa126734a 100644 --- a/src/uu/rmdir/src/rmdir.rs +++ b/src/uu/rmdir/src/rmdir.rs @@ -15,7 +15,7 @@ use uucore::display::Quotable; use uucore::error::{UResult, set_exit_code, strip_errno}; use uucore::translate; -use uucore::{format_usage, show_error, util_name}; +use uucore::{format_usage, show_error}; static OPT_IGNORE_FAIL_NON_EMPTY: &str = "ignore-fail-on-non-empty"; static OPT_PARENTS: &str = "parents"; @@ -114,7 +114,7 @@ fn remove_single(path: &Path, opts: Opts) -> Result<(), Error<'_>> { if opts.verbose { println!( "{}", - translate!("rmdir-verbose-removing-directory", "util_name" => util_name(), "path" => path.quote()) + translate!("rmdir-verbose-removing-directory", "util_name" => "rmdir", "path" => path.quote()) ); } remove_dir(path).map_err(|error| Error { error, path }) @@ -178,9 +178,9 @@ struct Opts { } pub fn uu_app() -> Command { - Command::new(util_name()) + Command::new("rmdir") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(util_name())) + .help_template(uucore::localized_help_template("rmdir")) .about(translate!("rmdir-about")) .override_usage(format_usage(&translate!("rmdir-usage"))) .infer_long_args(true) diff --git a/src/uu/runcon/src/runcon.rs b/src/uu/runcon/src/runcon.rs index 128d0dce370..1f6ab00dbd6 100644 --- a/src/uu/runcon/src/runcon.rs +++ b/src/uu/runcon/src/runcon.rs @@ -85,7 +85,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - let cmd = Command::new(uucore::util_name()) + let cmd = Command::new("runcon") .version(uucore::crate_version!()) .about(translate!("runcon-about")) .after_help(translate!("runcon-after-help")) diff --git a/src/uu/seq/src/seq.rs b/src/uu/seq/src/seq.rs index 0f32cf01f7c..3860f923bb4 100644 --- a/src/uu/seq/src/seq.rs +++ b/src/uu/seq/src/seq.rs @@ -228,7 +228,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("seq") .trailing_var_arg(true) .infer_long_args(true) .version(uucore::crate_version!()) diff --git a/src/uu/shred/src/shred.rs b/src/uu/shred/src/shred.rs index 7d19a42fc58..f35b1a0db2c 100644 --- a/src/uu/shred/src/shred.rs +++ b/src/uu/shred/src/shred.rs @@ -318,9 +318,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("shred") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("shred")) .about(translate!("shred-about")) .after_help(translate!("shred-after-help")) .override_usage(format_usage(&translate!("shred-usage"))) diff --git a/src/uu/shuf/src/shuf.rs b/src/uu/shuf/src/shuf.rs index de725cc4e20..d3dd00a1905 100644 --- a/src/uu/shuf/src/shuf.rs +++ b/src/uu/shuf/src/shuf.rs @@ -172,10 +172,10 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("shuf") .about(translate!("shuf-about")) .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("shuf")) .override_usage(format_usage(&translate!("shuf-usage"))) .infer_long_args(true) .arg( diff --git a/src/uu/sleep/src/sleep.rs b/src/uu/sleep/src/sleep.rs index 5b1a1f510a0..cd82aa59185 100644 --- a/src/uu/sleep/src/sleep.rs +++ b/src/uu/sleep/src/sleep.rs @@ -37,9 +37,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("sleep") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("sleep")) .about(translate!("sleep-about")) .after_help(translate!("sleep-after-help")) .override_usage(format_usage(&translate!("sleep-usage"))) diff --git a/src/uu/sort/Cargo.toml b/src/uu/sort/Cargo.toml index c648763764f..bf7c514bf3c 100644 --- a/src/uu/sort/Cargo.toml +++ b/src/uu/sort/Cargo.toml @@ -42,11 +42,12 @@ uucore = { workspace = true, features = [ "version-cmp", "i18n-decimal", "i18n-collator", + "i18n-datetime", ] } fluent = { workspace = true } foldhash = { workspace = true } -[target.'cfg(not(target_os = "redox"))'.dependencies] +[target.'cfg(not(any(target_os = "redox", target_os = "wasi")))'.dependencies] ctrlc = { workspace = true } [target.'cfg(unix)'.dependencies] @@ -61,6 +62,7 @@ uucore = { workspace = true, features = [ "parser-size", "version-cmp", "i18n-collator", + "i18n-datetime", ] } [[bin]] diff --git a/src/uu/sort/src/chunks.rs b/src/uu/sort/src/chunks.rs index dc1fab07033..ce6b2118be4 100644 --- a/src/uu/sort/src/chunks.rs +++ b/src/uu/sort/src/chunks.rs @@ -47,23 +47,42 @@ pub struct ChunkContents<'a> { pub line_count_hint: usize, } -#[derive(Debug)] +#[derive(Debug, Default)] pub struct LineData<'a> { pub selections: Vec<&'a [u8]>, pub num_infos: Vec, pub parsed_floats: Vec, pub line_num_floats: Vec>, + /// Arena buffer holding all collation sort keys concatenated. + pub collation_key_buffer: Vec, + /// End offsets into `collation_key_buffer` for each line's sort key. + pub collation_key_ends: Vec, +} + +impl LineData<'_> { + /// Get the collation sort key for a line at the given index. + pub fn collation_key(&self, index: usize) -> &[u8] { + let start = if index == 0 { + 0 + } else { + self.collation_key_ends[index - 1] + }; + let end = self.collation_key_ends[index]; + &self.collation_key_buffer[start..end] + } } impl Chunk { /// Destroy this chunk and return its components to be reused. pub fn recycle(mut self) -> RecycledChunk { - let recycled_contents = self.with_dependent_mut(|_, contents| { + let mut recycled_contents = self.with_dependent_mut(|_, contents| { contents.lines.clear(); contents.line_data.selections.clear(); contents.line_data.num_infos.clear(); contents.line_data.parsed_floats.clear(); contents.line_data.line_num_floats.clear(); + contents.line_data.collation_key_buffer.clear(); + contents.line_data.collation_key_ends.clear(); contents.token_buffer.clear(); let lines = unsafe { // SAFETY: It is safe to (temporarily) transmute to a vector of lines with a longer lifetime, @@ -81,26 +100,22 @@ impl Chunk { &mut contents.line_data.selections, )) }; - ( + RecycledChunk { lines, selections, - std::mem::take(&mut contents.line_data.num_infos), - std::mem::take(&mut contents.line_data.parsed_floats), - std::mem::take(&mut contents.line_data.line_num_floats), - std::mem::take(&mut contents.token_buffer), - contents.line_count_hint, - ) + num_infos: std::mem::take(&mut contents.line_data.num_infos), + parsed_floats: std::mem::take(&mut contents.line_data.parsed_floats), + line_num_floats: std::mem::take(&mut contents.line_data.line_num_floats), + collation_key_buffer: std::mem::take(&mut contents.line_data.collation_key_buffer), + collation_key_ends: std::mem::take(&mut contents.line_data.collation_key_ends), + token_buffer: std::mem::take(&mut contents.token_buffer), + line_count_hint: contents.line_count_hint, + // buffer is set below after we consume `self` + buffer: Vec::new(), + } }); - RecycledChunk { - lines: recycled_contents.0, - selections: recycled_contents.1, - num_infos: recycled_contents.2, - parsed_floats: recycled_contents.3, - line_num_floats: recycled_contents.4, - token_buffer: recycled_contents.5, - line_count_hint: recycled_contents.6, - buffer: self.into_owner(), - } + recycled_contents.buffer = self.into_owner(); + recycled_contents } pub fn lines(&self) -> &Vec> { @@ -118,6 +133,8 @@ pub struct RecycledChunk { num_infos: Vec, parsed_floats: Vec, line_num_floats: Vec>, + collation_key_buffer: Vec, + collation_key_ends: Vec, token_buffer: Vec>, line_count_hint: usize, buffer: Vec, @@ -131,6 +148,8 @@ impl RecycledChunk { num_infos: Vec::new(), parsed_floats: Vec::new(), line_num_floats: Vec::new(), + collation_key_buffer: Vec::new(), + collation_key_ends: Vec::new(), token_buffer: Vec::new(), line_count_hint: 0, buffer: vec![0; capacity], @@ -176,6 +195,8 @@ pub fn read( num_infos, parsed_floats, line_num_floats, + collation_key_buffer, + collation_key_ends, mut token_buffer, mut line_count_hint, mut buffer, @@ -214,6 +235,8 @@ pub fn read( num_infos, parsed_floats, line_num_floats, + collation_key_buffer, + collation_key_ends, }; parse_lines( read, @@ -253,6 +276,8 @@ fn parse_lines<'a>( assert!(line_data.num_infos.is_empty()); assert!(line_data.parsed_floats.is_empty()); assert!(line_data.line_num_floats.is_empty()); + assert!(line_data.collation_key_buffer.is_empty()); + assert!(line_data.collation_key_ends.is_empty()); token_buffer.clear(); const SMALL_CHUNK_BYTES: usize = 64 * 1024; let mut estimated = (*line_count_hint).max(1); @@ -406,3 +431,32 @@ fn read_to_buffer( } } } + +/// Parse a buffer into a `ChunkContents` suitable for `Chunk::try_new`. +/// Used by the WASI single-threaded sort path. +#[cfg(target_os = "wasi")] +pub fn parse_into_chunk<'a>( + buffer: &'a [u8], + separator: u8, + settings: &GlobalSettings, +) -> ChunkContents<'a> { + let mut lines = Vec::new(); + let mut line_data = LineData::default(); + let mut token_buffer = Vec::new(); + let mut line_count_hint = 0; + parse_lines( + buffer, + &mut lines, + &mut line_data, + &mut token_buffer, + &mut line_count_hint, + separator, + settings, + ); + ChunkContents { + lines, + line_data, + token_buffer, + line_count_hint, + } +} diff --git a/src/uu/sort/src/ext_sort/mod.rs b/src/uu/sort/src/ext_sort/mod.rs new file mode 100644 index 00000000000..099a4b72e62 --- /dev/null +++ b/src/uu/sort/src/ext_sort/mod.rs @@ -0,0 +1,20 @@ +// 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. + +//! External sort: sort large inputs that may not fit in memory. +//! +//! On most platforms this uses a multi-threaded chunked approach with +//! temporary files. On WASI (no threads) we fall back to an in-memory sort. + +#[cfg(not(target_os = "wasi"))] +mod threaded; +#[cfg(not(target_os = "wasi"))] +pub use threaded::ext_sort; + +#[cfg(target_os = "wasi")] +mod wasi; +#[cfg(target_os = "wasi")] +// `self::` needed to disambiguate from the `wasi` crate +pub use self::wasi::ext_sort; diff --git a/src/uu/sort/src/ext_sort.rs b/src/uu/sort/src/ext_sort/threaded.rs similarity index 94% rename from src/uu/sort/src/ext_sort.rs rename to src/uu/sort/src/ext_sort/threaded.rs index 59bf18a41a5..7dd089d0fe8 100644 --- a/src/uu/sort/src/ext_sort.rs +++ b/src/uu/sort/src/ext_sort/threaded.rs @@ -3,21 +3,15 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -//! Sort big files by using auxiliary files for storing intermediate chunks. -//! -//! Files are read into chunks of memory which are then sorted individually and -//! written to temporary files. There are two threads: One sorter, and one reader/writer. -//! The buffers for the individual chunks are recycled. There are two buffers. +//! Threaded external sort: read input in chunks, sort them in a background +//! thread, and spill to temporary files when memory is exceeded. use std::cmp::Ordering; use std::fs::File; -use std::io::{Write, stderr}; +use std::io::{Read, Write, stderr}; use std::path::PathBuf; -use std::{ - io::Read, - sync::mpsc::{Receiver, SyncSender}, - thread, -}; +use std::sync::mpsc::{Receiver, SyncSender}; +use std::thread; use itertools::Itertools; use uucore::error::{UResult, strip_errno}; @@ -29,17 +23,20 @@ use crate::merge::WriteablePlainTmpFile; use crate::merge::WriteableTmpFile; use crate::tmp_dir::TmpDirWrapper; use crate::{ - GlobalSettings, + GlobalSettings, Line, chunks::{self, Chunk}, - compare_by, merge, sort_by, + compare_by, merge, print_sorted, sort_by, }; -use crate::{Line, print_sorted}; // Note: update `test_sort::test_start_buffer` if this size is changed // Fixed to 8 KiB (equivalent to `std::sys::io::DEFAULT_BUF_SIZE` on most targets) const DEFAULT_BUF_SIZE: usize = 8 * 1024; /// Sort files by using auxiliary files for storing intermediate chunks (if needed), and output the result. +/// +/// Two threads cooperate: one reads input and writes temporary chunk files, +/// while the other sorts each chunk in memory. Once all chunks are written, +/// they are merged back together for final output. pub fn ext_sort( files: &mut impl Iterator>>, settings: &GlobalSettings, diff --git a/src/uu/sort/src/ext_sort/wasi.rs b/src/uu/sort/src/ext_sort/wasi.rs new file mode 100644 index 00000000000..50bd5f63033 --- /dev/null +++ b/src/uu/sort/src/ext_sort/wasi.rs @@ -0,0 +1,59 @@ +// 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. + +//! WASI single-threaded sort: read all input into memory, sort, and output. +//! Threads are not available on WASI, so we bypass the chunked/threaded path. + +use std::cmp::Ordering; +use std::io::Read; + +use itertools::Itertools; +use uucore::error::UResult; + +use crate::Output; +use crate::chunks::{self, Chunk}; +use crate::tmp_dir::TmpDirWrapper; +use crate::{GlobalSettings, compare_by, print_sorted, sort_by}; + +/// Sort files by reading all input into memory, sorting in a single thread, and outputting directly. +pub fn ext_sort( + files: &mut impl Iterator>>, + settings: &GlobalSettings, + output: Output, + _tmp_dir: &mut TmpDirWrapper, +) -> UResult<()> { + let separator = settings.line_ending.into(); + // Read all input into memory at once. Unlike the threaded path which uses + // chunked buffered reads, WASI has no threads so we accept the memory cost. + // Note: there is no size limit here — WASI targets are expected to handle + // moderately sized inputs; very large files may cause OOM. + let mut input = Vec::new(); + for file in files { + file?.read_to_end(&mut input)?; + } + if input.is_empty() { + return Ok(()); + } + let mut chunk = Chunk::try_new(input, |buffer| { + Ok::<_, Box>(chunks::parse_into_chunk( + buffer, separator, settings, + )) + })?; + chunk.with_dependent_mut(|_, contents| { + sort_by(&mut contents.lines, settings, &contents.line_data); + }); + if settings.unique { + print_sorted( + chunk.lines().iter().dedup_by(|a, b| { + compare_by(a, b, settings, chunk.line_data(), chunk.line_data()) == Ordering::Equal + }), + settings, + output, + )?; + } else { + print_sorted(chunk.lines().iter(), settings, output)?; + } + Ok(()) +} diff --git a/src/uu/sort/src/merge.rs b/src/uu/sort/src/merge.rs index d2bb3056b92..df03a3da612 100644 --- a/src/uu/sort/src/merge.rs +++ b/src/uu/sort/src/merge.rs @@ -571,6 +571,8 @@ impl MergeInput for CompressedTmpMergeInput { type InnerRead = ChildStdout; fn finished_reading(self) -> UResult<()> { + // Explicitly close stdout before waiting on the child process. + #[allow(clippy::drop_non_drop)] drop(self.child_stdout); check_child_success(self.child, &self.compress_prog)?; let _ = fs::remove_file(self.path); diff --git a/src/uu/sort/src/sort.rs b/src/uu/sort/src/sort.rs index 52d40511198..8b321b55d53 100644 --- a/src/uu/sort/src/sort.rs +++ b/src/uu/sort/src/sort.rs @@ -8,6 +8,7 @@ // https://www.gnu.org/software/coreutils/manual/html_node/sort-invocation.html // spell-checker:ignore (misc) HFKJFK Mbdfhn getrlimit RLIMIT_NOFILE rlim bigdecimal extendedbigdecimal hexdigit behaviour keydef GETFD localeconv foldhash +// spell-checker:ignore (misc) uppercased qsort getmonth juin juil mod buffer_hint; mod check; @@ -28,27 +29,32 @@ use foldhash::fast::FoldHasher; use foldhash::{HashMap, SharedSeed}; use numeric_str_cmp::{NumInfo, NumInfoParseSettings, human_numeric_str_cmp, numeric_str_cmp}; use rand::{RngExt as _, rng}; -use rayon::prelude::*; +#[cfg(not(target_os = "wasi"))] +use rayon::slice::ParallelSliceMut; use std::cmp::Ordering; use std::env; use std::ffi::{OsStr, OsString}; use std::fs::{File, OpenOptions}; use std::hash::{Hash, Hasher}; use std::io::{BufRead, BufReader, BufWriter, Read, Write, stdin, stdout}; -use std::num::{IntErrorKind, NonZero}; +use std::num::IntErrorKind; +#[cfg(not(target_os = "wasi"))] +use std::num::NonZero; use std::ops::Range; #[cfg(unix)] use std::os::unix::ffi::OsStrExt; use std::path::Path; use std::path::PathBuf; use std::str::Utf8Error; +use std::sync::OnceLock; use thiserror::Error; use uucore::display::Quotable; use uucore::error::{FromIo, strip_errno}; use uucore::error::{UError, UResult, USimpleError, UUsageError}; use uucore::extendedbigdecimal::ExtendedBigDecimal; #[cfg(feature = "i18n-collator")] -use uucore::i18n::collator::locale_cmp; +use uucore::i18n::collator::{compute_sort_key_utf8, locale_cmp}; +use uucore::i18n::datetime::get_locale_months; use uucore::i18n::decimal::locale_decimal_separator; use uucore::line_ending::LineEnding; use uucore::parser::num_parser::{ExtendedParser, ExtendedParserError}; @@ -324,6 +330,7 @@ struct Precomputed { floats_per_line: usize, selections_per_line: usize, fast_lexicographic: bool, + fast_locale_collation: bool, fast_ascii_insensitive: bool, tokenize_blank_thousands_sep: bool, tokenize_allow_unit_after_blank: bool, @@ -387,6 +394,8 @@ impl GlobalSettings { self.precomputed.fast_lexicographic = !disable_fast_lexicographic && self.can_use_fast_lexicographic(); + self.precomputed.fast_locale_collation = + disable_fast_lexicographic && self.can_use_fast_lexicographic(); self.precomputed.fast_ascii_insensitive = self.can_use_fast_ascii_insensitive(); } @@ -632,6 +641,15 @@ impl<'a> Line<'a> { token_buffer: &mut Vec, settings: &GlobalSettings, ) -> Self { + #[cfg(feature = "i18n-collator")] + if settings.precomputed.fast_locale_collation { + compute_sort_key_utf8(line, &mut line_data.collation_key_buffer); + line_data + .collation_key_ends + .push(line_data.collation_key_buffer.len()); + return Self { line, index }; + } + let needs_line_data = settings.precomputed.needs_tokens || settings.precomputed.selections_per_line > 0 || settings.precomputed.num_infos_per_line > 0 @@ -775,31 +793,23 @@ impl<'a> Line<'a> { } SortMode::Month => { let initial_selection = &self.line[selection.clone()]; - - let mut month_chars = initial_selection + let first_non_blank = initial_selection .iter() - .enumerate() - .skip_while(|(_, c)| c.is_ascii_whitespace()); + .position(|c| !c.is_ascii_whitespace()) + .unwrap_or(initial_selection.len()); - let month = if month_parse(initial_selection) == Month::Unknown { + let (parsed, match_len) = month_parse(initial_selection); + + if parsed == Month::Unknown { // We failed to parse a month, which is equivalent to matching nothing. // Add the "no match for key" marker to the first non-whitespace character. - let first_non_whitespace = month_chars.next(); - first_non_whitespace.map_or( - initial_selection.len()..initial_selection.len(), - |(idx, _)| idx..idx, - ) + selection.start += first_non_blank; + selection.end = selection.start; } else { - // We parsed a month. Match the first three non-whitespace characters, which must be the month we parsed. - month_chars.next().unwrap().0 - ..month_chars - .nth(2) - .map_or(initial_selection.len(), |(idx, _)| idx) - }; - - // Shorten selection to month. - selection.start += month.start; - selection.end = selection.start + month.len(); + // We parsed a month. Use the actual match byte length. + selection.start += first_non_blank; + selection.end = selection.start + match_len; + } } _ => {} } @@ -2030,7 +2040,11 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // sort errors with "cannot open: [...]" instead of "cannot read: [...]" here let reader = open_with_open_failed_error(&files0_from)?; let buf_reader = BufReader::new(reader); - for (line_num, line) in buf_reader.split(b'\0').flatten().enumerate() { + for (line_num, line_res) in buf_reader.split(b'\0').enumerate() { + let line = line_res.map_err(|error| SortError::ReadFailed { + path: files0_from.clone(), + error, + })?; let f = std::str::from_utf8(&line) .expect("Could not parse string from zero terminated input."); match f { @@ -2112,13 +2126,16 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { settings.threads = matches .get_one::(options::PARALLEL) .map_or_else(|| "0".to_string(), String::from); - let num_threads = match settings.threads.parse::() { - Ok(0) | Err(_) => std::thread::available_parallelism().map_or(1, NonZero::get), - Ok(n) => n, - }; - let _ = rayon::ThreadPoolBuilder::new() - .num_threads(num_threads) - .build_global(); + #[cfg(not(target_os = "wasi"))] + { + let num_threads = match settings.threads.parse::() { + Ok(0) | Err(_) => std::thread::available_parallelism().map_or(1, NonZero::get), + Ok(n) => n, + }; + let _ = rayon::ThreadPoolBuilder::new() + .num_threads(num_threads) + .build_global(); + } } if let Some(size_str) = matches.get_one::(options::BUF_SIZE) { @@ -2131,11 +2148,22 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { settings.buffer_size_is_explicit = false; } - let mut tmp_dir = TmpDirWrapper::new( - matches - .get_one::(options::TMP_DIR) - .map_or_else(env::temp_dir, PathBuf::from), - ); + let mut tmp_dir = TmpDirWrapper::new(matches.get_one::(options::TMP_DIR).map_or_else( + || { + // WASI does not support std::env::temp_dir() — it panics with + // "no filesystem on wasm". Use /tmp as a nominal fallback; + // the WASI ext_sort path never actually creates temp files. + #[cfg(target_os = "wasi")] + { + PathBuf::from("/tmp") + } + #[cfg(not(target_os = "wasi"))] + { + env::temp_dir() + } + }, + PathBuf::from, + )); settings.compress_prog = matches .get_one::(options::COMPRESS_PROG) @@ -2328,7 +2356,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { uucore::clap_localization::configure_localized_command( - Command::new(uucore::util_name()) + Command::new("sort") .version(uucore::crate_version!()) .about(translate!("sort-about")) .after_help(translate!("sort-after-help")) @@ -2591,10 +2619,19 @@ fn exec( } fn sort_by<'a>(unsorted: &mut Vec>, settings: &GlobalSettings, line_data: &LineData<'a>) { + let cmp = |a: &Line<'a>, b: &Line<'a>| compare_by(a, b, settings, line_data, line_data); + // WASI does not support threads, so use non-parallel sort to avoid + // rayon's thread pool which triggers an unreachable trap. if settings.stable || settings.unique { - unsorted.par_sort_by(|a, b| compare_by(a, b, settings, line_data, line_data)); + #[cfg(not(target_os = "wasi"))] + unsorted.par_sort_by(cmp); + #[cfg(target_os = "wasi")] + unsorted.sort_by(cmp); } else { - unsorted.par_sort_unstable_by(|a, b| compare_by(a, b, settings, line_data, line_data)); + #[cfg(not(target_os = "wasi"))] + unsorted.par_sort_unstable_by(cmp); + #[cfg(target_os = "wasi")] + unsorted.sort_unstable_by(cmp); } } @@ -2614,6 +2651,18 @@ fn compare_by<'a>( }; } + #[cfg(feature = "i18n-collator")] + if global_settings.precomputed.fast_locale_collation { + let a_key = a_line_data.collation_key(a.index); + let b_key = b_line_data.collation_key(b.index); + let cmp = a_key.cmp(b_key); + return if global_settings.reverse { + cmp.reverse() + } else { + cmp + }; + } + if global_settings.precomputed.fast_ascii_insensitive { let cmp = ascii_case_insensitive_cmp(a.line, b.line); if cmp != Ordering::Equal || a.line == b.line { @@ -2993,30 +3042,88 @@ enum Month { December, } +/// Cached locale month lookup table. +/// Each entry is (uppercased_name, month_value). +type MonthTable = Vec<(Vec, Month)>; + +fn get_locale_month_table() -> Option<&'static MonthTable> { + static TABLE: OnceLock> = OnceLock::new(); + + TABLE + .get_or_init(|| { + let months = get_locale_months()?; + let all_months = [ + Month::January, + Month::February, + Month::March, + Month::April, + Month::May, + Month::June, + Month::July, + Month::August, + Month::September, + Month::October, + Month::November, + Month::December, + ]; + let table: Vec<(Vec, Month)> = months + .iter() + .zip(all_months.iter()) + .map(|(name, &month)| (name.clone(), month)) + .collect(); + Some(table) + }) + .as_ref() +} + /// Parse the beginning string into a Month, returning [`Month::Unknown`] on errors. -fn month_parse(line: &[u8]) -> Month { +/// Also returns the byte length consumed from the input (after leading blanks). +/// +/// The stored locale month names have blanks stripped and are uppercased. +/// Comparison against input is case-insensitive but NOT blank-insensitive: +/// the input must match the stored name exactly (after leading blank trimming). +fn month_parse(line: &[u8]) -> (Month, usize) { let line = line.trim_ascii_start(); + // Try locale-specific month names, keeping the longest match. + // This handles cases where one name is a prefix of another + // (e.g., Japanese "1" vs "10", "11", "12"). + if let Some(table) = get_locale_month_table() { + let mut best = None; + for (name, month) in table { + if line.len() >= name.len() + && line[..name.len()].eq_ignore_ascii_case(name) + && best.as_ref().is_none_or(|&(len, _)| name.len() > len) + { + best = Some((name.len(), *month)); + } + } + if let Some((len, month)) = best { + return (month, len); + } + } + + // Fall back to English 3-letter abbreviations match line.get(..3).map(<[u8]>::to_ascii_uppercase).as_deref() { - Some(b"JAN") => Month::January, - Some(b"FEB") => Month::February, - Some(b"MAR") => Month::March, - Some(b"APR") => Month::April, - Some(b"MAY") => Month::May, - Some(b"JUN") => Month::June, - Some(b"JUL") => Month::July, - Some(b"AUG") => Month::August, - Some(b"SEP") => Month::September, - Some(b"OCT") => Month::October, - Some(b"NOV") => Month::November, - Some(b"DEC") => Month::December, - _ => Month::Unknown, + Some(b"JAN") => (Month::January, 3), + Some(b"FEB") => (Month::February, 3), + Some(b"MAR") => (Month::March, 3), + Some(b"APR") => (Month::April, 3), + Some(b"MAY") => (Month::May, 3), + Some(b"JUN") => (Month::June, 3), + Some(b"JUL") => (Month::July, 3), + Some(b"AUG") => (Month::August, 3), + Some(b"SEP") => (Month::September, 3), + Some(b"OCT") => (Month::October, 3), + Some(b"NOV") => (Month::November, 3), + Some(b"DEC") => (Month::December, 3), + _ => (Month::Unknown, 0), } } fn month_compare(a: &[u8], b: &[u8]) -> Ordering { - let ma = month_parse(a); - let mb = month_parse(b); + let ma = month_parse(a).0; + let mb = month_parse(b).0; ma.cmp(&mb) } diff --git a/src/uu/sort/src/tmp_dir.rs b/src/uu/sort/src/tmp_dir.rs index 2ec48232003..0e5bd1f34d4 100644 --- a/src/uu/sort/src/tmp_dir.rs +++ b/src/uu/sort/src/tmp_dir.rs @@ -2,18 +2,21 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. + +#[cfg(not(any(target_os = "redox", target_os = "wasi")))] +use std::path::Path; +#[cfg(not(any(target_os = "redox", target_os = "wasi")))] use std::sync::atomic::{AtomicBool, Ordering}; use std::{ fs::File, - path::{Path, PathBuf}, + path::PathBuf, sync::{Arc, LazyLock, Mutex}, }; use tempfile::TempDir; -use uucore::{ - error::{UResult, USimpleError}, - show_error, translate, -}; +use uucore::error::UResult; +#[cfg(not(any(target_os = "redox", target_os = "wasi")))] +use uucore::{error::USimpleError, show_error, translate}; use crate::{SortError, current_open_fd_count, fd_soft_limit}; @@ -51,7 +54,7 @@ fn should_install_signal_handler() -> bool { open_fds.saturating_add(CTRL_C_FDS + RESERVED_FOR_MERGE) <= limit } -#[cfg(not(target_os = "redox"))] +#[cfg(not(any(target_os = "redox", target_os = "wasi")))] fn ensure_signal_handler_installed(state: Arc>) -> UResult<()> { // This shared state must originate from `HANDLER_STATE` so the handler always sees // the current lock/path pair and can clean up the active temp directory on SIGINT. @@ -101,7 +104,8 @@ fn ensure_signal_handler_installed(state: Arc>) -> UR Ok(()) } -#[cfg(target_os = "redox")] +#[cfg(any(target_os = "redox", target_os = "wasi"))] +#[allow(clippy::unnecessary_wraps)] fn ensure_signal_handler_installed(_state: Arc>) -> UResult<()> { Ok(()) } @@ -181,6 +185,7 @@ impl Drop for TmpDirWrapper { /// Remove the directory at `path` by deleting its child files and then itself. /// Errors while deleting child files are ignored. +#[cfg(not(any(target_os = "redox", target_os = "wasi")))] fn remove_tmp_dir(path: &Path) -> std::io::Result<()> { if let Ok(read_dir) = std::fs::read_dir(path) { for file in read_dir.flatten() { diff --git a/src/uu/split/locales/fr-FR.ftl b/src/uu/split/locales/fr-FR.ftl index e8e70c280fa..832e923a4fd 100644 --- a/src/uu/split/locales/fr-FR.ftl +++ b/src/uu/split/locales/fr-FR.ftl @@ -23,7 +23,7 @@ split-error-multi-character-separator = séparateur multi-caractères { $separat split-error-multiple-separator-characters = plusieurs caractères de séparateur spécifiés split-error-filter-with-kth-chunk = --filter ne traite pas un chunk extrait vers stdout split-error-invalid-io-block-size = taille de bloc IO invalide : { $size } -split-error-not-supported = --filter n'est actuellement pas supporté sur cette plateforme +split-error-not-supported = --filter n'est actuellement pas pris en charge sur cette plateforme split-error-invalid-number-of-chunks = nombre de chunks invalide : { $chunks } split-error-invalid-chunk-number = numéro de chunk invalide : { $chunk } split-error-invalid-number-of-lines = nombre de lignes invalide : { $error } diff --git a/src/uu/split/src/filenames.rs b/src/uu/split/src/filenames.rs index 83b43943bed..68dc35aa215 100644 --- a/src/uu/split/src/filenames.rs +++ b/src/uu/split/src/filenames.rs @@ -334,7 +334,7 @@ impl<'a> FilenameIterator<'a> { } impl Iterator for FilenameIterator<'_> { - type Item = String; + type Item = OsString; fn next(&mut self) -> Option { if self.first_iteration { @@ -344,12 +344,10 @@ impl Iterator for FilenameIterator<'_> { } // The first and third parts are just taken directly from the // struct parameters unchanged. - Some(format!( - "{}{}{}", - self.prefix.to_string_lossy(), - self.number, - self.additional_suffix.to_string_lossy() - )) + let mut filename = self.prefix.to_os_string(); + filename.push(self.number.to_string()); + filename.push(self.additional_suffix); + Some(filename) } } @@ -512,4 +510,29 @@ mod tests { let it = FilenameIterator::new(std::ffi::OsStr::new("chunk_"), &suffix); assert!(it.is_err()); } + #[test] + #[cfg(unix)] + fn test_filename_iterator_preserves_non_utf8_bytes() { + use std::ffi::OsStr; + use std::os::unix::ffi::{OsStrExt, OsStringExt}; + + let suffix = Suffix { + stype: SuffixType::Alphabetic, + length: 2, + start: 0, + auto_widening: false, + additional: std::ffi::OsString::from_vec(vec![0xFE]), + }; + + let mut it = FilenameIterator::new(OsStr::from_bytes(b"p\xFF"), &suffix) + .expect("valid fixed-width filename iterator"); + assert_eq!( + it.next().expect("first chunk filename exists").into_vec(), + b"p\xFFaa\xFE".to_vec() + ); + assert_eq!( + it.next().expect("second chunk filename exists").into_vec(), + b"p\xFFab\xFE".to_vec() + ); + } } diff --git a/src/uu/split/src/platform/mod.rs b/src/uu/split/src/platform/mod.rs index 5afc43eeb7f..33dc874586e 100644 --- a/src/uu/split/src/platform/mod.rs +++ b/src/uu/split/src/platform/mod.rs @@ -12,6 +12,34 @@ pub use self::windows::instantiate_current_writer; #[cfg(windows)] pub use self::windows::paths_refer_to_same_file; +// WASI: no process spawning (filter) or device/inode comparison. +#[cfg(target_os = "wasi")] +pub fn paths_refer_to_same_file(_p1: &std::ffi::OsStr, _p2: &std::ffi::OsStr) -> bool { + false +} + +#[cfg(target_os = "wasi")] +pub fn instantiate_current_writer( + _filter: Option<&str>, + filename: &std::ffi::OsStr, + is_new: bool, +) -> std::io::Result>> { + let file = if is_new { + std::fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(std::path::Path::new(filename))? + } else { + std::fs::OpenOptions::new() + .append(true) + .open(std::path::Path::new(filename))? + }; + Ok(std::io::BufWriter::new( + Box::new(file) as Box + )) +} + #[cfg(unix)] mod unix; diff --git a/src/uu/split/src/platform/unix.rs b/src/uu/split/src/platform/unix.rs index 656bd0109be..c9e0964cc31 100644 --- a/src/uu/split/src/platform/unix.rs +++ b/src/uu/split/src/platform/unix.rs @@ -3,11 +3,12 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. use std::env; -use std::ffi::OsStr; +use std::ffi::{OsStr, OsString}; use std::io::{BufWriter, Error, Result}; use std::io::{ErrorKind, Write}; use std::path::Path; use std::process::{Child, Command, Stdio}; +use uucore::display::Quotable; use uucore::error::USimpleError; use uucore::fs; use uucore::fs::FileInformation; @@ -45,12 +46,12 @@ struct WithEnvVarSet { /// Env var key previous_var_key: String, /// Previous value set to this key - previous_var_value: std::result::Result, + previous_var_value: Option, } impl WithEnvVarSet { /// Save previous value assigned to key, set key=value - fn new(key: &str, value: &str) -> Self { - let previous_env_value = env::var(key); + fn new(key: &str, value: &OsStr) -> Self { + let previous_env_value = env::var_os(key); unsafe { env::set_var(key, value); } @@ -64,7 +65,7 @@ impl WithEnvVarSet { impl Drop for WithEnvVarSet { /// Restore previous value now that this is being dropped by context fn drop(&mut self) { - if let Ok(ref prev_value) = self.previous_var_value { + if let Some(prev_value) = &self.previous_var_value { unsafe { env::set_var(&self.previous_var_key, prev_value); } @@ -82,7 +83,7 @@ impl FilterWriter { /// /// * `command` - The shell command to execute /// * `filepath` - Path of the output file (forwarded to command as $FILE) - fn new(command: &str, filepath: &str) -> Result { + fn new(command: &str, filepath: &OsStr) -> Result { // set $FILE, save previous value (if there was one) let _with_env_var_set = WithEnvVarSet::new("FILE", filepath); @@ -127,7 +128,7 @@ impl Drop for FilterWriter { /// Instantiate either a file writer or a "write to shell process's stdin" writer pub fn instantiate_current_writer( filter: Option<&str>, - filename: &str, + filename: &OsStr, is_new: bool, ) -> Result>> { match filter { @@ -138,24 +139,25 @@ pub fn instantiate_current_writer( .write(true) .create(true) .truncate(true) - .open(Path::new(&filename)) + .open(Path::new(filename)) .map_err(|e| match e.kind() { ErrorKind::IsADirectory => Error::other( - translate!("split-error-is-a-directory", "dir" => filename), + translate!("split-error-is-a-directory", "dir" => filename.quote()), ), _ => Error::other( - translate!("split-error-unable-to-open-file", "file" => filename), + translate!("split-error-unable-to-open-file", "file" => filename.quote()), ), })? } else { // re-open file that we previously created to append to it std::fs::OpenOptions::new() .append(true) - .open(Path::new(&filename)) + .open(Path::new(filename)) .map_err(|_| { - Error::other( - translate!("split-error-unable-to-reopen-file", "file" => filename), - ) + Error::other(translate!( + "split-error-unable-to-reopen-file", + "file" => filename.quote() + )) })? }; Ok(BufWriter::new(Box::new(file) as Box)) diff --git a/src/uu/split/src/platform/windows.rs b/src/uu/split/src/platform/windows.rs index 6693e4fe909..3efef2240b9 100644 --- a/src/uu/split/src/platform/windows.rs +++ b/src/uu/split/src/platform/windows.rs @@ -6,6 +6,7 @@ use std::ffi::OsStr; use std::io::{BufWriter, Error, Result}; use std::io::{ErrorKind, Write}; use std::path::Path; +use uucore::display::Quotable; use uucore::fs; use uucore::translate; @@ -15,7 +16,7 @@ use uucore::translate; /// a file writer pub fn instantiate_current_writer( _filter: Option<&str>, - filename: &str, + filename: &OsStr, is_new: bool, ) -> Result>> { let file = if is_new { @@ -24,22 +25,24 @@ pub fn instantiate_current_writer( .write(true) .create(true) .truncate(true) - .open(Path::new(&filename)) + .open(Path::new(filename)) .map_err(|e| match e.kind() { - ErrorKind::IsADirectory => { - Error::other(translate!("split-error-is-a-directory", "dir" => filename)) - } - _ => { - Error::other(translate!("split-error-unable-to-open-file", "file" => filename)) - } + ErrorKind::IsADirectory => Error::other( + translate!("split-error-is-a-directory", "dir" => filename.quote()), + ), + _ => Error::other( + translate!("split-error-unable-to-open-file", "file" => filename.quote()), + ), })? } else { // re-open file that we previously created to append to it std::fs::OpenOptions::new() .append(true) - .open(Path::new(&filename)) + .open(Path::new(filename)) .map_err(|_| { - Error::other(translate!("split-error-unable-to-reopen-file", "file" => filename)) + Error::other( + translate!("split-error-unable-to-reopen-file", "file" => filename.quote()), + ) })? }; Ok(BufWriter::new(Box::new(file) as Box)) diff --git a/src/uu/split/src/split.rs b/src/uu/split/src/split.rs index 923f66ac134..1540950c172 100644 --- a/src/uu/split/src/split.rs +++ b/src/uu/split/src/split.rs @@ -14,7 +14,7 @@ use crate::filenames::{FilenameIterator, Suffix, SuffixError}; use crate::strategy::{NumberType, Strategy, StrategyError}; use clap::{Arg, ArgAction, ArgMatches, Command, ValueHint, parser::ValueSource}; use std::env; -use std::ffi::OsString; +use std::ffi::{OsStr, OsString}; use std::fs::{File, metadata}; use std::io; use std::io::{BufRead, BufReader, BufWriter, ErrorKind, Read, Seek, SeekFrom, Write, stdin}; @@ -234,7 +234,7 @@ fn handle_preceding_options( } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("split") .version(uucore::crate_version!()) .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("split-about")) @@ -540,10 +540,10 @@ impl Settings { fn instantiate_current_writer( &self, - filename: &str, + filename: &OsStr, is_new: bool, ) -> io::Result>> { - if platform::paths_refer_to_same_file(&self.input, filename.as_ref()) { + if platform::paths_refer_to_same_file(&self.input, filename) { return Err(io::Error::other( translate!("split-error-would-overwrite-input", "file" => filename.quote()), )); @@ -920,7 +920,7 @@ impl Write for LineChunkWriter<'_> { /// Output file parameters struct OutFile { - filename: String, + filename: OsString, maybe_writer: Option>>, is_new: bool, } @@ -974,7 +974,7 @@ impl ManageOutFiles for OutFiles { let maybe_writer = if is_writer_optional { None } else { - let instantiated = settings.instantiate_current_writer(filename.as_str(), true); + let instantiated = settings.instantiate_current_writer(&filename, true); // If there was an error instantiating the writer for a file, // it could be due to hitting the system limit of open files, // so record it as None and let [`get_writer`] function handle closing/re-opening @@ -1011,7 +1011,7 @@ impl ManageOutFiles for OutFiles { // might "steel" the freed fd and open a file on its side. Then it would be beneficial // if split would be able to close another fd before cancellation. 'loop1: loop { - let filename_to_open = self[idx].filename.as_str(); + let filename_to_open = &self[idx].filename; let file_to_open_is_new = self[idx].is_new; let maybe_writer = settings.instantiate_current_writer(filename_to_open, file_to_open_is_new); diff --git a/src/uu/stat/src/stat.rs b/src/uu/stat/src/stat.rs index e91e0f3eb93..468f9ed6c23 100644 --- a/src/uu/stat/src/stat.rs +++ b/src/uu/stat/src/stat.rs @@ -1353,9 +1353,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("stat") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("stat")) .about(translate!("stat-about")) .after_help(translate!("stat-after-help")) .override_usage(format_usage(&translate!("stat-usage"))) diff --git a/src/uu/stdbuf/Cargo.toml b/src/uu/stdbuf/Cargo.toml index da02c726c1b..7510be1a4f4 100644 --- a/src/uu/stdbuf/Cargo.toml +++ b/src/uu/stdbuf/Cargo.toml @@ -21,7 +21,7 @@ path = "src/stdbuf.rs" [dependencies] clap = { workspace = true } -libstdbuf = { package = "uu_stdbuf_libstdbuf", version = "0.7.0", path = "src/libstdbuf" } +libstdbuf = { package = "uu_stdbuf_libstdbuf", version = "0.8.0", path = "src/libstdbuf" } tempfile = { workspace = true } uucore = { workspace = true, features = ["parser-size"] } thiserror = { workspace = true } diff --git a/src/uu/stdbuf/locales/en-US.ftl b/src/uu/stdbuf/locales/en-US.ftl index f2f845a07b6..50269efc3ee 100644 --- a/src/uu/stdbuf/locales/en-US.ftl +++ b/src/uu/stdbuf/locales/en-US.ftl @@ -23,7 +23,6 @@ stdbuf-value-mode = MODE stdbuf-error-line-buffering-stdin-meaningless = line buffering stdin is meaningless stdbuf-error-invalid-mode = invalid mode {$error} stdbuf-error-value-too-large = invalid mode '{$value}': Value too large for defined data type -stdbuf-error-command-not-supported = Command not supported for this operating system! stdbuf-error-external-libstdbuf-not-found = External libstdbuf not found at configured path: {$path} stdbuf-error-permission-denied = failed to execute process: Permission denied stdbuf-error-no-such-file = failed to execute process: No such file or directory diff --git a/src/uu/stdbuf/locales/fr-FR.ftl b/src/uu/stdbuf/locales/fr-FR.ftl index 5016c6914f1..b371361aec2 100644 --- a/src/uu/stdbuf/locales/fr-FR.ftl +++ b/src/uu/stdbuf/locales/fr-FR.ftl @@ -23,7 +23,6 @@ stdbuf-value-mode = MODE stdbuf-error-line-buffering-stdin-meaningless = la mise en mémoire tampon par ligne de stdin n'a pas de sens stdbuf-error-invalid-mode = mode invalide {$error} stdbuf-error-value-too-large = mode invalide '{$value}' : Valeur trop grande pour le type de données défini -stdbuf-error-command-not-supported = Commande non prise en charge pour ce système d'exploitation ! stdbuf-error-external-libstdbuf-not-found = libstdbuf externe introuvable au chemin configuré : {$path} stdbuf-error-permission-denied = échec de l'exécution du processus : Permission refusée stdbuf-error-no-such-file = échec de l'exécution du processus : Aucun fichier ou répertoire de ce type diff --git a/src/uu/stdbuf/src/libstdbuf/LICENSE b/src/uu/stdbuf/src/libstdbuf/LICENSE new file mode 120000 index 00000000000..2a64f9d0fc6 --- /dev/null +++ b/src/uu/stdbuf/src/libstdbuf/LICENSE @@ -0,0 +1 @@ +../../../../../LICENSE \ No newline at end of file diff --git a/src/uu/stdbuf/src/libstdbuf/src/libstdbuf.rs b/src/uu/stdbuf/src/libstdbuf/src/libstdbuf.rs index 26d8a9829d5..d22b8c871f4 100644 --- a/src/uu/stdbuf/src/libstdbuf/src/libstdbuf.rs +++ b/src/uu/stdbuf/src/libstdbuf/src/libstdbuf.rs @@ -2,12 +2,12 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (ToDO) getreent reent IOFBF IOLBF IONBF setvbuf stderrp stdinp stdoutp +// spell-checker:ignore (ToDO) getreent reent IOFBF IOLBF IONBF setvbuf stderrp stdinp stdoutp fdopen use ctor::ctor; use libc::{_IOFBF, _IOLBF, _IONBF, FILE, c_char, c_int, fileno, size_t}; -use std::env; -use std::ptr; +use std::io::{Write, stderr}; +use std::{env, ptr}; // This runs automatically when the library is loaded via LD_PRELOAD #[ctor] @@ -35,6 +35,11 @@ pub unsafe extern "C" fn __stdbuf_get_stdin() -> *mut FILE { unsafe { __stdin } } + #[cfg(target_os = "netbsd")] + { + unsafe { libc::fdopen(0, c"r".as_ptr()) } + } + #[cfg(target_os = "cygwin")] { // _getreent()->_std{in,out,err} @@ -61,6 +66,7 @@ pub unsafe extern "C" fn __stdbuf_get_stdin() -> *mut FILE { #[cfg(not(any( target_os = "macos", target_os = "freebsd", + target_os = "netbsd", target_os = "openbsd", target_os = "cygwin" )))] @@ -92,6 +98,11 @@ pub unsafe extern "C" fn __stdbuf_get_stdout() -> *mut FILE { unsafe { __stdout } } + #[cfg(target_os = "netbsd")] + { + unsafe { libc::fdopen(1, c"w".as_ptr()) } + } + #[cfg(target_os = "cygwin")] { // _getreent()->_std{in,out,err} @@ -118,6 +129,7 @@ pub unsafe extern "C" fn __stdbuf_get_stdout() -> *mut FILE { #[cfg(not(any( target_os = "macos", target_os = "freebsd", + target_os = "netbsd", target_os = "openbsd", target_os = "cygwin" )))] @@ -149,6 +161,11 @@ pub unsafe extern "C" fn __stdbuf_get_stderr() -> *mut FILE { unsafe { __stderr } } + #[cfg(target_os = "netbsd")] + { + unsafe { libc::fdopen(2, c"w".as_ptr()) } + } + #[cfg(target_os = "cygwin")] { // _getreent()->_std{in,out,err} @@ -175,6 +192,7 @@ pub unsafe extern "C" fn __stdbuf_get_stderr() -> *mut FILE { #[cfg(not(any( target_os = "macos", target_os = "freebsd", + target_os = "netbsd", target_os = "openbsd", target_os = "cygwin" )))] @@ -192,7 +210,7 @@ fn set_buffer(stream: *mut FILE, value: &str) { "L" => (_IOLBF, 0_usize), input => { let Ok(buff_size) = input.parse::() else { - eprintln!("failed to allocate a {value} byte stdio buffer"); + let _ = writeln!(stderr(), "failed to allocate a {value} byte stdio buffer"); std::process::exit(1); }; (_IOFBF, buff_size as size_t) @@ -205,9 +223,11 @@ fn set_buffer(stream: *mut FILE, value: &str) { res = libc::setvbuf(stream, buffer, mode, size); } if res != 0 { - eprintln!("could not set buffering of {} to mode {mode}", unsafe { - fileno(stream) - }); + let _ = writeln!( + stderr(), + "could not set buffering of {} to mode {mode}", + unsafe { fileno(stream) } + ); } } diff --git a/src/uu/stdbuf/src/stdbuf.rs b/src/uu/stdbuf/src/stdbuf.rs index 773b5ff9ccc..0bf64922bdc 100644 --- a/src/uu/stdbuf/src/stdbuf.rs +++ b/src/uu/stdbuf/src/stdbuf.rs @@ -4,6 +4,8 @@ // file that was distributed with this source code. // spell-checker:ignore (ToDO) tempdir dyld dylib optgrps libstdbuf +#[cfg(not(unix))] +compile_error!("stdbuf is not supported on the target"); use clap::{Arg, ArgAction, ArgMatches, Command}; use std::ffi::OsString; @@ -31,14 +33,9 @@ mod options { #[cfg(all( not(feature = "feat_external_libstdbuf"), - any( - target_os = "linux", - target_os = "android", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd", - target_os = "dragonfly" - ) + unix, + not(target_vendor = "apple"), + not(target_os = "cygwin") ))] const STDBUF_INJECT: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/libstdbuf.so")); @@ -82,45 +79,19 @@ enum ProgramOptionsError { ValueTooLarge(String), } -#[cfg(any( - target_os = "linux", - target_os = "android", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd", - target_os = "dragonfly" -))] -#[expect( - clippy::unnecessary_wraps, - reason = "fn sig must match on all platforms" -)] -fn preload_strings() -> UResult<(&'static str, &'static str)> { - Ok(("LD_PRELOAD", "so")) +#[cfg(all(unix, not(target_vendor = "apple"), not(target_os = "cygwin")))] +fn preload_strings() -> (&'static str, &'static str) { + ("LD_PRELOAD", "so") } #[cfg(target_vendor = "apple")] -#[expect( - clippy::unnecessary_wraps, - reason = "fn sig must match on all platforms" -)] -fn preload_strings() -> UResult<(&'static str, &'static str)> { - Ok(("DYLD_LIBRARY_PATH", "dylib")) +fn preload_strings() -> (&'static str, &'static str) { + ("DYLD_LIBRARY_PATH", "dylib") } -#[cfg(not(any( - target_os = "linux", - target_os = "android", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd", - target_os = "dragonfly", - target_vendor = "apple" -)))] -fn preload_strings() -> UResult<(&'static str, &'static str)> { - Err(USimpleError::new( - 1, - translate!("stdbuf-error-command-not-supported"), - )) +#[cfg(target_os = "cygwin")] +fn preload_strings() -> (&'static str, &'static str) { + ("LD_PRELOAD", "dll") } fn check_option(matches: &ArgMatches, name: &str) -> Result { @@ -163,7 +134,7 @@ fn get_preload_env(tmp_dir: &TempDir) -> UResult<(String, PathBuf)> { use std::fs::File; use std::io::Write; - let (preload, extension) = preload_strings()?; + let (preload, extension) = preload_strings(); let inject_path = tmp_dir.path().join("libstdbuf").with_extension(extension); let mut file = File::create(&inject_path)?; @@ -174,7 +145,7 @@ fn get_preload_env(tmp_dir: &TempDir) -> UResult<(String, PathBuf)> { #[cfg(feature = "feat_external_libstdbuf")] fn get_preload_env(_tmp_dir: &TempDir) -> UResult<(String, PathBuf)> { - let (preload, extension) = preload_strings()?; + let (preload, extension) = preload_strings(); // Use the directory provided at compile time via LIBSTDBUF_DIR environment variable // This will fail to compile if LIBSTDBUF_DIR is not set, which is the desired behavior @@ -183,7 +154,7 @@ fn get_preload_env(_tmp_dir: &TempDir) -> UResult<(String, PathBuf)> { // Search paths in order: // 1. Directory where stdbuf is located (program_path) // 2. Compile-time directory from LIBSTDBUF_DIR - let mut search_paths: Vec = Vec::new(); + let mut search_paths: Vec = Vec::with_capacity(2); // First, try to get the directory where stdbuf is running from if let Ok(exe_path) = std::env::current_exe() { @@ -259,9 +230,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("stdbuf") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("stdbuf")) .about(translate!("stdbuf-about")) .after_help(translate!("stdbuf-after-help")) .override_usage(format_usage(&translate!("stdbuf-usage"))) diff --git a/src/uu/stty/src/stty.rs b/src/uu/stty/src/stty.rs index 6909d2c4f5f..686f00a823e 100644 --- a/src/uu/stty/src/stty.rs +++ b/src/uu/stty/src/stty.rs @@ -1253,9 +1253,9 @@ fn get_sane_control_char(cc_index: S) -> u8 { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("stty") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("stty")) .override_usage(format_usage(&translate!("stty-usage"))) .about(translate!("stty-about")) .infer_long_args(true) diff --git a/src/uu/sync/src/sync.rs b/src/uu/sync/src/sync.rs index b91f216aaed..b34d0ac0bd1 100644 --- a/src/uu/sync/src/sync.rs +++ b/src/uu/sync/src/sync.rs @@ -68,10 +68,13 @@ mod platform { // Reset O_NONBLOCK flag if it was set (matches GNU behavior) // This is non-critical, so we log errors but don't fail if let Err(e) = fcntl(&f, FcntlArg::F_SETFL(OFlag::empty())) { - eprintln!( + use std::io::{Write, stderr}; + let _ = writeln!( + stderr(), "sync: {}", translate!("sync-warning-fcntl-failed", "file" => path, "error" => e.to_string()) ); + uucore::error::set_exit_code(1); } Ok(f) } @@ -260,8 +263,12 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { #[allow(clippy::if_same_then_else)] if matches.get_flag(options::FILE_SYSTEM) { - #[cfg(any(target_os = "linux", target_os = "android", target_os = "windows"))] - syncfs(files)?; + if files.is_empty() { + sync()?; + } else { + #[cfg(any(target_os = "linux", target_os = "android", target_os = "windows"))] + syncfs(files)?; + } } else if matches.get_flag(options::DATA) { #[cfg(any(target_os = "linux", target_os = "android"))] fdatasync(files)?; @@ -272,9 +279,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("sync") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("sync")) .about(translate!("sync-about")) .override_usage(format_usage(&translate!("sync-usage"))) .infer_long_args(true) diff --git a/src/uu/tac/src/tac.rs b/src/uu/tac/src/tac.rs index 88c355b0e4b..50e615c988b 100644 --- a/src/uu/tac/src/tac.rs +++ b/src/uu/tac/src/tac.rs @@ -56,9 +56,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("tac") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("tac")) .override_usage(format_usage(&translate!("tac-usage"))) .about(translate!("tac-about")) .infer_long_args(true) diff --git a/src/uu/tail/Cargo.toml b/src/uu/tail/Cargo.toml index 5e11a54119c..eaf269f1097 100644 --- a/src/uu/tail/Cargo.toml +++ b/src/uu/tail/Cargo.toml @@ -21,15 +21,17 @@ path = "src/tail.rs" [dependencies] clap = { workspace = true } -libc = { workspace = true } memchr = { workspace = true } -notify = { workspace = true } uucore = { workspace = true, features = ["fs", "parser-size", "signals"] } same-file = { workspace = true } fluent = { workspace = true } +[target.'cfg(not(target_os = "wasi"))'.dependencies] +libc = { workspace = true } +notify = { workspace = true } + [target.'cfg(unix)'.dependencies] -nix = { workspace = true, features = ["fs"] } +rustix = { workspace = true, features = ["fs"] } [target.'cfg(windows)'.dependencies] windows-sys = { workspace = true, features = [ diff --git a/src/uu/tail/locales/en-US.ftl b/src/uu/tail/locales/en-US.ftl index 6d434ae9862..44b9a7c6288 100644 --- a/src/uu/tail/locales/en-US.ftl +++ b/src/uu/tail/locales/en-US.ftl @@ -58,10 +58,8 @@ tail-status-has-been-replaced-following-new-file = { $file } has been replaced; tail-status-file-truncated = { $file }: file truncated tail-status-replaced-with-untailable-file = { $file } has been replaced with an untailable file tail-status-replaced-with-untailable-file-giving-up = { $file } has been replaced with an untailable file; giving up on this name -tail-status-file-became-inaccessible = { $file } { $become_inaccessible }: { $no_such_file } tail-status-directory-containing-watched-file-removed = directory containing watched file was removed tail-status-backend-cannot-be-used-reverting-to-polling = { $backend } cannot be used, reverting to polling -tail-status-file-no-such-file = { $file }: { $no_such_file } # Text constants tail-bad-fd = Bad file descriptor diff --git a/src/uu/tail/locales/fr-FR.ftl b/src/uu/tail/locales/fr-FR.ftl index 85d973571ae..39f0a5a7ebb 100644 --- a/src/uu/tail/locales/fr-FR.ftl +++ b/src/uu/tail/locales/fr-FR.ftl @@ -1,8 +1,8 @@ tail-about = Afficher les 10 dernières lignes de chaque FICHIER sur la sortie standard. Avec plus d'un FICHIER, précéder chacun d'un en-tête donnant le nom du fichier. Sans FICHIER, ou quand FICHIER est -, lire l'entrée standard. - Les arguments obligatoires pour les drapeaux longs sont également obligatoires pour les drapeaux courts. -tail-usage = tail [DRAPEAU]... [FICHIER]... + Les arguments obligatoires pour les options longues sont également obligatoires pour les options courtes. +tail-usage = tail [OPTION]... [FICHIER]... # Messages d'aide tail-help-bytes = Nombre d'octets à afficher @@ -57,10 +57,8 @@ tail-status-has-been-replaced-following-new-file = { $file } a été remplacé ; tail-status-file-truncated = { $file } : fichier tronqué tail-status-replaced-with-untailable-file = { $file } a été remplacé par un fichier non suivable tail-status-replaced-with-untailable-file-giving-up = { $file } a été remplacé par un fichier non suivable ; abandon de ce nom -tail-status-file-became-inaccessible = { $file } { $become_inaccessible } : { $no_such_file } tail-status-directory-containing-watched-file-removed = le répertoire contenant le fichier surveillé a été supprimé tail-status-backend-cannot-be-used-reverting-to-polling = { $backend } ne peut pas être utilisé, retour au sondage -tail-status-file-no-such-file = { $file } : { $no_such_file } # Constantes de texte tail-bad-fd = Descripteur de fichier incorrect diff --git a/src/uu/tail/src/args.rs b/src/uu/tail/src/args.rs index e1c1fe9aee1..155f7cc1d53 100644 --- a/src/uu/tail/src/args.rs +++ b/src/uu/tail/src/args.rs @@ -447,10 +447,12 @@ pub fn uu_app() -> Command { let polling_help = translate!("tail-help-polling-unix"); #[cfg(target_os = "windows")] let polling_help = translate!("tail-help-polling-windows"); + #[cfg(not(any(unix, target_os = "windows")))] + let polling_help = translate!("tail-help-polling-unix"); - Command::new(uucore::util_name()) + Command::new("tail") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("tail")) .about(translate!("tail-about")) .override_usage(format_usage(&translate!("tail-usage"))) .infer_long_args(true) diff --git a/src/uu/tail/src/follow/mod.rs b/src/uu/tail/src/follow/mod.rs index 604602a4b01..2cd9b3af7fc 100644 --- a/src/uu/tail/src/follow/mod.rs +++ b/src/uu/tail/src/follow/mod.rs @@ -3,7 +3,70 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +#[cfg(not(target_os = "wasi"))] mod files; +#[cfg(not(target_os = "wasi"))] mod watch; +#[cfg(not(target_os = "wasi"))] pub use watch::{Observer, follow}; + +// WASI: notify/inotify are unavailable, so `tail -f` cannot work. +// Provide minimal stubs matching the real Observer API so tail compiles. +#[cfg(target_os = "wasi")] +mod wasi_stubs { + use crate::args::Settings; + use std::io::BufRead; + use std::path::Path; + use uucore::error::{UResult, USimpleError}; + + pub struct Observer { + pub use_polling: bool, + } + + impl Observer { + pub fn from(_settings: &Settings) -> Self { + Self { use_polling: false } + } + + #[allow(clippy::unnecessary_wraps)] + pub fn start(&mut self, _settings: &Settings) -> UResult<()> { + Ok(()) + } + + #[allow(clippy::unnecessary_wraps)] + pub fn add_path( + &mut self, + _path: &Path, + _display_name: &str, + _reader: Option>, + _update_last: bool, + ) -> UResult<()> { + Ok(()) + } + + #[allow(clippy::unnecessary_wraps)] + pub fn add_bad_path( + &mut self, + _path: &Path, + _display_name: &str, + _update_last: bool, + ) -> UResult<()> { + Ok(()) + } + + pub fn follow_name_retry(&self) -> bool { + false + } + } + + pub fn follow(_observer: Observer, _settings: &Settings) -> UResult<()> { + Err(USimpleError::new( + 1, + "follow mode is not supported on this platform", + )) + } +} + +#[cfg(target_os = "wasi")] +pub use wasi_stubs::{Observer, follow}; diff --git a/src/uu/tail/src/follow/watch.rs b/src/uu/tail/src/follow/watch.rs index b3f88c224a3..592c866dbf9 100644 --- a/src/uu/tail/src/follow/watch.rs +++ b/src/uu/tail/src/follow/watch.rs @@ -394,8 +394,10 @@ impl Observer { if let Some(old_md) = self.files.get_mut_metadata(event_path) { if old_md.is_tailable() && self.files.get(event_path).reader.is_some() { show_error!( - "{}", - translate!("tail-status-file-became-inaccessible", "file" => display_name.quote(), "become_inaccessible" => translate!("tail-become-inaccessible"), "no_such_file" => translate!("tail-no-such-file-or-directory")) + "{} {}: {}", + display_name.quote(), + translate!("tail-become-inaccessible"), + translate!("tail-no-such-file-or-directory") ); } } @@ -410,8 +412,9 @@ impl Observer { } } else { show_error!( - "{}", - translate!("tail-status-file-no-such-file", "file" => display_name, "no_such_file" => translate!("tail-no-such-file-or-directory")) + "{}: {}", + display_name, + translate!("tail-no-such-file-or-directory") ); if !self.files.files_remaining() && self.use_polling { // NOTE: GNU's tail exits here for `---disable-inotify` diff --git a/src/uu/tail/src/paths.rs b/src/uu/tail/src/paths.rs index 6eaeae9801b..8682cb571af 100644 --- a/src/uu/tail/src/paths.rs +++ b/src/uu/tail/src/paths.rs @@ -12,6 +12,7 @@ use std::io::{Seek, SeekFrom}; #[cfg(unix)] use std::os::unix::fs::{FileTypeExt, MetadataExt}; use std::path::{Path, PathBuf}; +#[cfg(not(target_os = "wasi"))] use uucore::error::UResult; use uucore::translate; @@ -157,7 +158,9 @@ impl FileExtTail for File { pub trait MetadataExtTail { fn is_tailable(&self) -> bool; + #[cfg(not(target_os = "wasi"))] fn got_truncated(&self, other: &Metadata) -> UResult; + #[cfg(not(target_os = "wasi"))] fn file_id_eq(&self, other: &Metadata) -> bool; } @@ -175,10 +178,12 @@ impl MetadataExtTail for Metadata { } /// Return true if the file was modified and is now shorter + #[cfg(not(target_os = "wasi"))] fn got_truncated(&self, other: &Metadata) -> UResult { Ok(other.len() < self.len() && other.modified()? != self.modified()?) } + #[cfg(not(target_os = "wasi"))] fn file_id_eq(&self, #[cfg(unix)] other: &Metadata, #[cfg(not(unix))] _: &Metadata) -> bool { #[cfg(unix)] { @@ -200,12 +205,14 @@ impl MetadataExtTail for Metadata { } } +#[cfg(not(target_os = "wasi"))] pub trait PathExtTail { fn is_stdin(&self) -> bool; fn is_orphan(&self) -> bool; fn is_tailable(&self) -> bool; } +#[cfg(not(target_os = "wasi"))] impl PathExtTail for Path { fn is_stdin(&self) -> bool { self.eq(Self::new(text::DASH)) diff --git a/src/uu/tail/src/platform/mod.rs b/src/uu/tail/src/platform/mod.rs index d3220491f4a..bb77501fdcb 100644 --- a/src/uu/tail/src/platform/mod.rs +++ b/src/uu/tail/src/platform/mod.rs @@ -14,6 +14,15 @@ pub use self::unix::{ #[cfg(windows)] pub use self::windows::{Pid, ProcessChecker, supports_pid_checks}; +// WASI has no process management; provide stubs so tail compiles. +#[cfg(target_os = "wasi")] +pub type Pid = u64; + +#[cfg(target_os = "wasi")] +pub fn supports_pid_checks(_pid: Pid) -> bool { + false +} + #[cfg(unix)] mod unix; diff --git a/src/uu/tail/src/tail.rs b/src/uu/tail/src/tail.rs index 5b0c6d75c36..6bd812a1eb9 100644 --- a/src/uu/tail/src/tail.rs +++ b/src/uu/tail/src/tail.rs @@ -220,7 +220,7 @@ fn tail_file( /// Without `--pid`, FIFOs block on open() until a writer connects (GNU behavior). #[cfg(unix)] fn open_file(path: &Path, use_nonblock_for_fifo: bool) -> io::Result { - use nix::fcntl::{FcntlArg, OFlag, fcntl}; + use rustix::fs::{OFlags, fcntl_getfl, fcntl_setfl}; use std::fs::OpenOptions; use std::os::fd::AsFd; use std::os::unix::fs::{FileTypeExt, OpenOptionsExt}; @@ -237,9 +237,9 @@ fn open_file(path: &Path, use_nonblock_for_fifo: bool) -> io::Result { .open(path)?; // Clear O_NONBLOCK so reads block normally - let flags = fcntl(file.as_fd(), FcntlArg::F_GETFL)?; - let new_flags = OFlag::from_bits_truncate(flags) & !OFlag::O_NONBLOCK; - fcntl(file.as_fd(), FcntlArg::F_SETFL(new_flags))?; + let flags = fcntl_getfl(file.as_fd())?; + let new_flags = flags & !OFlags::NONBLOCK; + fcntl_setfl(file.as_fd(), new_flags)?; Ok(file) } else { diff --git a/src/uu/tail/src/text.rs b/src/uu/tail/src/text.rs index 0c4972345cf..b4874643028 100644 --- a/src/uu/tail/src/text.rs +++ b/src/uu/tail/src/text.rs @@ -18,3 +18,5 @@ pub const BACKEND: &str = "inotify"; pub const BACKEND: &str = "kqueue"; #[cfg(target_os = "windows")] pub const BACKEND: &str = "ReadDirectoryChanges"; +#[cfg(not(any(unix, target_os = "windows")))] +pub const BACKEND: &str = "polling"; diff --git a/src/uu/tee/locales/en-US.ftl b/src/uu/tee/locales/en-US.ftl index 4db948e6c3b..47bd49c74a0 100644 --- a/src/uu/tee/locales/en-US.ftl +++ b/src/uu/tee/locales/en-US.ftl @@ -14,7 +14,7 @@ tee-help-output-error-exit = exit on write errors to any output tee-help-output-error-exit-nopipe = exit on write errors to any output that are not pipe errors (equivalent to exit on non-unix platforms) # Error messages -tee-error-stdin = stdin: { $error } +tee-error-stdin = read error: { $error } # Other messages tee-standard-output = 'standard output' diff --git a/src/uu/tee/locales/fr-FR.ftl b/src/uu/tee/locales/fr-FR.ftl index eeea09ac492..e86faf9b6df 100644 --- a/src/uu/tee/locales/fr-FR.ftl +++ b/src/uu/tee/locales/fr-FR.ftl @@ -14,7 +14,7 @@ tee-help-output-error-exit = quitter en cas d'erreurs d'écriture vers toute sor tee-help-output-error-exit-nopipe = quitter en cas d'erreurs d'écriture vers toute sortie qui ne sont pas des erreurs de tube (équivalent à exit sur les plateformes non-unix) # Messages d'erreur -tee-error-stdin = stdin : { $error } +tee-error-stdin = erreur de lecture: { $error } # Autres messages tee-standard-output = 'sortie standard' diff --git a/src/uu/tee/src/cli.rs b/src/uu/tee/src/cli.rs new file mode 100644 index 00000000000..9ad54405061 --- /dev/null +++ b/src/uu/tee/src/cli.rs @@ -0,0 +1,103 @@ +// 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 nopipe + +use clap::{Arg, ArgAction, Command, builder::PossibleValue}; +use std::ffi::OsString; +use uucore::parser::shortcut_value_parser::ShortcutValueParser; +pub use uucore::{format_usage, translate}; + +pub mod options { + pub const APPEND: &str = "append"; + pub const IGNORE_INTERRUPTS: &str = "ignore-interrupts"; + pub const FILE: &str = "file"; + pub const IGNORE_PIPE_ERRORS: &str = "ignore-pipe-errors"; + pub const OUTPUT_ERROR: &str = "output-error"; +} + +#[derive(Clone, Debug)] +pub enum OutputErrorMode { + /// Diagnose write error on any output + Warn, + /// Diagnose write error on any output that is not a pipe + WarnNoPipe, + /// Exit upon write error on any output + Exit, + /// Exit upon write error on any output that is not a pipe + ExitNoPipe, +} + +#[allow(dead_code)] +pub struct Options { + pub append: bool, + pub ignore_interrupts: bool, + pub ignore_pipe_errors: bool, + pub files: Vec, + pub output_error: Option, +} + +pub fn uu_app() -> Command { + Command::new("tee") + .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template("tee")) + .about(translate!("tee-about")) + .override_usage(format_usage(&translate!("tee-usage"))) + .after_help(translate!("tee-after-help")) + .infer_long_args(true) + // Since we use value-specific help texts for "--output-error", clap's "short help" and "long help" differ. + // However, this is something that the GNU tests explicitly test for, so we *always* show the long help instead. + .disable_help_flag(true) + .arg( + Arg::new("--help") + .short('h') + .long("help") + .help(translate!("tee-help-help")) + .action(ArgAction::HelpLong), + ) + .arg( + Arg::new(options::APPEND) + .long(options::APPEND) + .short('a') + .help(translate!("tee-help-append")) + .action(ArgAction::SetTrue) + .overrides_with(options::APPEND), + ) + .arg( + Arg::new(options::IGNORE_INTERRUPTS) + .long(options::IGNORE_INTERRUPTS) + .short('i') + .help(translate!("tee-help-ignore-interrupts")) + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new(options::FILE) + .action(ArgAction::Append) + .value_hint(clap::ValueHint::FilePath) + .value_parser(clap::value_parser!(OsString)), + ) + .arg( + Arg::new(options::IGNORE_PIPE_ERRORS) + .short('p') + .help(translate!("tee-help-ignore-pipe-errors")) + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new(options::OUTPUT_ERROR) + .long(options::OUTPUT_ERROR) + .require_equals(true) + .num_args(0..=1) + .default_missing_value("warn-nopipe") + .value_parser(ShortcutValueParser::new([ + PossibleValue::new("warn").help(translate!("tee-help-output-error-warn")), + PossibleValue::new("warn-nopipe") + .help(translate!("tee-help-output-error-warn-nopipe")), + PossibleValue::new("exit").help(translate!("tee-help-output-error-exit")), + PossibleValue::new("exit-nopipe") + .help(translate!("tee-help-output-error-exit-nopipe")), + ])) + .help(translate!("tee-help-output-error")), + ) +} diff --git a/src/uu/tee/src/tee.rs b/src/uu/tee/src/tee.rs index b9912c42ebf..272083b5c41 100644 --- a/src/uu/tee/src/tee.rs +++ b/src/uu/tee/src/tee.rs @@ -3,53 +3,25 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use clap::{Arg, ArgAction, Command, builder::PossibleValue}; +// spell-checker:ignore espidf nopipe + use std::ffi::OsString; use std::fs::OpenOptions; use std::io::{Error, ErrorKind, Read, Result, Write, stderr, stdin, stdout}; use std::path::PathBuf; use uucore::display::Quotable; -use uucore::error::UResult; -use uucore::format_usage; -use uucore::parser::shortcut_value_parser::ShortcutValueParser; +use uucore::error::{UResult, strip_errno}; use uucore::translate; -// spell-checker:ignore nopipe +mod cli; +pub use crate::cli::uu_app; +use crate::cli::{Options, OutputErrorMode, options}; #[cfg(target_os = "linux")] use uucore::signals::ensure_stdout_not_broken; #[cfg(unix)] use uucore::signals::{disable_pipe_errors, ignore_interrupts}; -mod options { - pub const APPEND: &str = "append"; - pub const IGNORE_INTERRUPTS: &str = "ignore-interrupts"; - pub const FILE: &str = "file"; - pub const IGNORE_PIPE_ERRORS: &str = "ignore-pipe-errors"; - pub const OUTPUT_ERROR: &str = "output-error"; -} - -#[allow(dead_code)] -struct Options { - append: bool, - ignore_interrupts: bool, - ignore_pipe_errors: bool, - files: Vec, - output_error: Option, -} - -#[derive(Clone, Debug)] -enum OutputErrorMode { - /// Diagnose write error on any output - Warn, - /// Diagnose write error on any output that is not a pipe - WarnNoPipe, - /// Exit upon write error on any output - Exit, - /// Exit upon write error on any output that is not a pipe - ExitNoPipe, -} - #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?; @@ -57,24 +29,16 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let append = matches.get_flag(options::APPEND); let ignore_interrupts = matches.get_flag(options::IGNORE_INTERRUPTS); let ignore_pipe_errors = matches.get_flag(options::IGNORE_PIPE_ERRORS); - let output_error = if matches.contains_id(options::OUTPUT_ERROR) { - match matches - .get_one::(options::OUTPUT_ERROR) - .map(String::as_str) - { - Some("warn") => Some(OutputErrorMode::Warn), - // If no argument is specified for --output-error, - // defaults to warn-nopipe - None | Some("warn-nopipe") => Some(OutputErrorMode::WarnNoPipe), - Some("exit") => Some(OutputErrorMode::Exit), - Some("exit-nopipe") => Some(OutputErrorMode::ExitNoPipe), - _ => unreachable!(), - } - } else if ignore_pipe_errors { - Some(OutputErrorMode::WarnNoPipe) - } else { - None - }; + let output_error = matches + .get_one::(options::OUTPUT_ERROR) + .map(|s| match s.as_str() { + "warn" => OutputErrorMode::Warn, + "warn-nopipe" => OutputErrorMode::WarnNoPipe, + "exit" => OutputErrorMode::Exit, + "exit-nopipe" => OutputErrorMode::ExitNoPipe, + _ => unreachable!("clap excluded it"), + }) + .or_else(|| ignore_pipe_errors.then_some(OutputErrorMode::WarnNoPipe)); let files = matches .get_many::(options::FILE) @@ -92,68 +56,6 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { tee(&options).map_err(|_| 1.into()) } -pub fn uu_app() -> Command { - Command::new(uucore::util_name()) - .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) - .about(translate!("tee-about")) - .override_usage(format_usage(&translate!("tee-usage"))) - .after_help(translate!("tee-after-help")) - .infer_long_args(true) - // Since we use value-specific help texts for "--output-error", clap's "short help" and "long help" differ. - // However, this is something that the GNU tests explicitly test for, so we *always* show the long help instead. - .disable_help_flag(true) - .arg( - Arg::new("--help") - .short('h') - .long("help") - .help(translate!("tee-help-help")) - .action(ArgAction::HelpLong), - ) - .arg( - Arg::new(options::APPEND) - .long(options::APPEND) - .short('a') - .help(translate!("tee-help-append")) - .action(ArgAction::SetTrue) - .overrides_with(options::APPEND), - ) - .arg( - Arg::new(options::IGNORE_INTERRUPTS) - .long(options::IGNORE_INTERRUPTS) - .short('i') - .help(translate!("tee-help-ignore-interrupts")) - .action(ArgAction::SetTrue), - ) - .arg( - Arg::new(options::FILE) - .action(ArgAction::Append) - .value_hint(clap::ValueHint::FilePath) - .value_parser(clap::value_parser!(OsString)), - ) - .arg( - Arg::new(options::IGNORE_PIPE_ERRORS) - .short('p') - .help(translate!("tee-help-ignore-pipe-errors")) - .action(ArgAction::SetTrue), - ) - .arg( - Arg::new(options::OUTPUT_ERROR) - .long(options::OUTPUT_ERROR) - .require_equals(true) - .num_args(0..=1) - .value_parser(ShortcutValueParser::new([ - PossibleValue::new("warn").help(translate!("tee-help-output-error-warn")), - PossibleValue::new("warn-nopipe") - .help(translate!("tee-help-output-error-warn-nopipe")), - PossibleValue::new("exit").help(translate!("tee-help-output-error-exit")), - PossibleValue::new("exit-nopipe") - .help(translate!("tee-help-output-error-exit-nopipe")), - ])) - .help(translate!("tee-help-output-error")), - ) -} - fn tee(options: &Options) -> Result<()> { #[cfg(unix)] { @@ -178,14 +80,12 @@ fn tee(options: &Options) -> Result<()> { 0, NamedWriter { name: translate!("tee-standard-output").into(), - inner: Box::new(stdout()), + inner: Writer::Stdout(stdout()), }, ); let mut output = MultiWriter::new(writers, options.output_error.clone()); - let input = &mut NamedReader { - inner: Box::new(stdin()) as Box, - }; + let input = NamedReader { inner: stdin() }; #[cfg(target_os = "linux")] if options.ignore_pipe_errors && !ensure_stdout_not_broken()? && output.writers.len() == 1 { @@ -218,35 +118,46 @@ fn copy(mut input: impl Read, mut output: impl Write) -> Result { // the standard library: // https://github.com/rust-lang/rust/blob/2feb91181882e525e698c4543063f4d0296fcf91/library/std/src/io/copy.rs#L271-L297 - // Use buffer size from std implementation: + // Use small buffer size from std implementation for small input // https://github.com/rust-lang/rust/blob/2feb91181882e525e698c4543063f4d0296fcf91/library/std/src/sys/io/mod.rs#L44 - // spell-checker:ignore espidf - const DEFAULT_BUF_SIZE: usize = if cfg!(target_os = "espidf") { + const FIRST_BUF_SIZE: usize = if cfg!(target_os = "espidf") { 512 } else { 8 * 1024 }; - - let mut buffer = [0u8; DEFAULT_BUF_SIZE]; + let mut buffer = [0u8; FIRST_BUF_SIZE]; let mut len = 0; + match input.read(&mut buffer) { + Ok(0) => return Ok(0), + Ok(bytes_count) => { + output.write_all(&buffer[0..bytes_count])?; + len = bytes_count; + if bytes_count < FIRST_BUF_SIZE { + // flush the buffer to comply with POSIX requirement that + // `tee` does not buffer the input. + output.flush()?; + return Ok(len); + } + } + Err(e) if e.kind() == ErrorKind::Interrupted => (), + Err(e) => return Err(e), + } + // but optimize buffer size also for large file + let mut buffer = vec![0u8; 4 * FIRST_BUF_SIZE]; //stack array makes code path for smaller file slower loop { - let received = match input.read(&mut buffer) { - Ok(bytes_count) => bytes_count, - Err(e) if e.kind() == ErrorKind::Interrupted => continue, + match input.read(&mut buffer) { + Ok(0) => return Ok(len), // end of file + Ok(received) => { + output.write_all(&buffer[..received])?; + // flush the buffer to comply with POSIX requirement that + // `tee` does not buffer the input. + output.flush()?; + len += received; + } + Err(e) if e.kind() == ErrorKind::Interrupted => {} Err(e) => return Err(e), - }; - - if received == 0 { - return Ok(len); } - - output.write_all(&buffer[0..received])?; - - // We need to flush the buffer here to comply with POSIX requirement that - // `tee` does not buffer the input. - output.flush()?; - len += received; } } @@ -267,7 +178,7 @@ fn open( }; match mode.write(true).create(true).open(path.as_path()) { Ok(file) => Some(Ok(NamedWriter { - inner: Box::new(file), + inner: Writer::File(file), name: name.clone(), })), Err(f) => { @@ -393,8 +304,29 @@ impl Write for MultiWriter { } } +enum Writer { + File(std::fs::File), + Stdout(std::io::Stdout), +} + +impl Write for Writer { + fn write(&mut self, buf: &[u8]) -> Result { + match self { + Self::File(f) => f.write(buf), + Self::Stdout(s) => s.write(buf), + } + } + + fn flush(&mut self) -> Result<()> { + match self { + Self::File(f) => f.flush(), + Self::Stdout(s) => s.flush(), + } + } +} + struct NamedWriter { - inner: Box, + inner: Writer, pub name: OsString, } @@ -409,14 +341,18 @@ impl Write for NamedWriter { } struct NamedReader { - inner: Box, + inner: std::io::Stdin, } impl Read for NamedReader { fn read(&mut self, buf: &mut [u8]) -> Result { match self.inner.read(buf) { Err(f) => { - let _ = writeln!(stderr(), "{}", translate!("tee-error-stdin", "error" => f)); + let _ = writeln!( + stderr(), + "tee: {}", + translate!("tee-error-stdin", "error" => strip_errno(&f)) + ); Err(f) } okay => okay, diff --git a/src/uu/test/src/test.rs b/src/uu/test/src/test.rs index 92b1dcdde02..38c0072e15e 100644 --- a/src/uu/test/src/test.rs +++ b/src/uu/test/src/test.rs @@ -33,7 +33,8 @@ use uucore::translate; pub fn uu_app() -> Command { // Disable printing of -h and -v as valid alternatives for --help and --version, // since we don't recognize -h and -v as help/version flags. - Command::new(uucore::util_name()) + // We change the name to test later + Command::new("[") .version(uucore::crate_version!()) .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("test-about")) @@ -64,8 +65,10 @@ pub fn uumain(mut args: impl uucore::Args) -> UResult<()> { translate!("test-error-missing-closing-bracket"), )); } + } else { + // Show actual name with error + let _ = uu_app().name("test"); } - let result = parse(args).map(|mut stack| eval(&mut stack))??; if result { Ok(()) } else { Err(1.into()) } diff --git a/src/uu/timeout/Cargo.toml b/src/uu/timeout/Cargo.toml index 90a0b378c4a..ecc10470195 100644 --- a/src/uu/timeout/Cargo.toml +++ b/src/uu/timeout/Cargo.toml @@ -30,3 +30,11 @@ nix = { workspace = true, features = ["signal"] } [[bin]] name = "timeout" path = "src/main.rs" + +[dev-dependencies] +divan = { workspace = true } +uucore = { workspace = true, features = ["benchmark"] } + +[[bench]] +name = "timeout_bench" +harness = false diff --git a/src/uu/timeout/benches/timeout_bench.rs b/src/uu/timeout/benches/timeout_bench.rs new file mode 100644 index 00000000000..511955f6f0b --- /dev/null +++ b/src/uu/timeout/benches/timeout_bench.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. + +#[cfg(unix)] +use divan::{Bencher, black_box}; +#[cfg(unix)] +use uu_timeout::uumain; +#[cfg(unix)] +use uucore::benchmark::run_util_function; + +/// Benchmark the fast path where the command exits immediately. +#[cfg(unix)] +#[divan::bench] +fn timeout_quick_exit(bencher: Bencher) { + bencher.bench(|| { + black_box(run_util_function(uumain, &["0.02", "true"])); + }); +} + +/// Benchmark a command that runs longer than the threshold and receives the default signal. +#[cfg(unix)] +#[divan::bench] +fn timeout_enforced(bencher: Bencher) { + bencher.bench(|| { + black_box(run_util_function(uumain, &["0.02", "sleep", "0.2"])); + }); +} + +fn main() { + #[cfg(unix)] + divan::main(); +} diff --git a/src/uu/timeout/src/timeout.rs b/src/uu/timeout/src/timeout.rs index 6944794f19c..25590ebd611 100644 --- a/src/uu/timeout/src/timeout.rs +++ b/src/uu/timeout/src/timeout.rs @@ -22,7 +22,7 @@ use uucore::translate; use uucore::{ format_usage, - signals::{signal_by_name_or_value, signal_name_by_value}, + signals::{signal_by_name_or_value, signal_list_name_by_value}, }; use nix::sys::signal::{SigHandler, Signal, kill}; @@ -125,7 +125,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new("timeout") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("timeout")) .about(translate!("timeout-about")) .override_usage(format_usage(&translate!("timeout-usage"))) .arg( @@ -227,7 +227,7 @@ fn report_if_verbose(signal: usize, cmd: &str, verbose: bool) { let s = if signal == 0 { "0".to_string() } else { - signal_name_by_value(signal).unwrap().to_string() + signal_list_name_by_value(signal).unwrap() }; let mut stderr = std::io::stderr(); let _ = writeln!( @@ -296,7 +296,7 @@ fn wait_or_kill_process( }); Ok(exit_code) } else { - Ok(ExitStatus::TimeoutFailed.into()) + Ok(ExitStatus::CommandTimedOut.into()) } } Ok(None) => { diff --git a/src/uu/touch/Cargo.toml b/src/uu/touch/Cargo.toml index cbcda5ae4a2..4bfff3d5df1 100644 --- a/src/uu/touch/Cargo.toml +++ b/src/uu/touch/Cargo.toml @@ -29,7 +29,8 @@ uucore = { workspace = true, features = ["libc", "parser"] } fluent = { workspace = true } [target.'cfg(unix)'.dependencies] -nix = { workspace = true, features = ["fs"] } +libc = { workspace = true } +rustix = { workspace = true, features = ["fs"] } [dev-dependencies] tempfile = { workspace = true } diff --git a/src/uu/touch/src/touch.rs b/src/uu/touch/src/touch.rs index d1080c1dddf..bcb688ee7b3 100644 --- a/src/uu/touch/src/touch.rs +++ b/src/uu/touch/src/touch.rs @@ -16,11 +16,11 @@ use jiff::fmt::strtime; use jiff::tz::TimeZone; use jiff::{Timestamp, ToSpan, Zoned}; #[cfg(unix)] -use nix::libc::O_NONBLOCK; +use libc::O_NONBLOCK; #[cfg(unix)] -use nix::sys::stat::futimens; +use rustix::fs::Timestamps; #[cfg(unix)] -use nix::sys::time::TimeSpec; +use rustix::fs::futimens; use std::borrow::Cow; use std::ffi::{OsStr, OsString}; #[cfg(unix)] @@ -263,9 +263,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("touch") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("touch")) .about(translate!("touch-about")) .override_usage(format_usage(&translate!("touch-usage"))) .infer_long_args(true) @@ -617,28 +617,18 @@ fn try_futimens_via_write_fd(path: &Path, atime: FileTime, mtime: FileTime) -> s .custom_flags(O_NONBLOCK) .open(path)?; - let atime_sec = atime.unix_seconds(); - let atime_nsec = i64::from(atime.nanoseconds()); - let mtime_sec = mtime.unix_seconds(); - let mtime_nsec = i64::from(mtime.nanoseconds()); - - #[cfg(target_pointer_width = "32")] - let atime_spec = TimeSpec::new( - atime_sec.try_into().unwrap(), - atime_nsec.try_into().unwrap(), - ); - #[cfg(target_pointer_width = "64")] - let atime_spec = TimeSpec::new(atime_sec, atime_nsec); - - #[cfg(target_pointer_width = "32")] - let mtime_spec = TimeSpec::new( - mtime_sec.try_into().unwrap(), - mtime_nsec.try_into().unwrap(), - ); - #[cfg(target_pointer_width = "64")] - let mtime_spec = TimeSpec::new(mtime_sec, mtime_nsec); - - futimens(&file, &atime_spec, &mtime_spec).map_err(Error::from) + let timestamps = Timestamps { + last_access: rustix::fs::Timespec { + tv_sec: atime.unix_seconds(), + tv_nsec: atime.nanoseconds() as _, + }, + last_modification: rustix::fs::Timespec { + tv_sec: mtime.unix_seconds(), + tv_nsec: mtime.nanoseconds() as _, + }, + }; + + futimens(&file, ×tamps).map_err(|e| Error::from_raw_os_error(e.raw_os_error())) } /// Get metadata of the provided path @@ -883,6 +873,10 @@ fn pathbuf_from_stdout() -> Result { .map_err(|e| TouchError::WindowsStdoutPathError(e.to_string()))? .into()) } + #[cfg(target_os = "wasi")] + { + Ok(PathBuf::from("/dev/stdout")) + } } #[cfg(test)] diff --git a/src/uu/tr/src/operation.rs b/src/uu/tr/src/operation.rs index f945bebca7c..616c911c4dc 100644 --- a/src/uu/tr/src/operation.rs +++ b/src/uu/tr/src/operation.rs @@ -185,8 +185,7 @@ impl Sequence { .chain(33..=47) .chain(58..=64) .chain(91..=96) - .chain(123..=126) - .chain(std::iter::once(32)), // space + .chain(123..=126), ), Class::Print => Box::new( (48..=57) // digit @@ -196,7 +195,8 @@ impl Sequence { .chain(33..=47) .chain(58..=64) .chain(91..=96) - .chain(123..=126), + .chain(123..=126) + .chain(std::iter::once(32)), // space ), Class::Punct => Box::new((33..=47).chain(58..=64).chain(91..=96).chain(123..=126)), Class::Space => Box::new(unicode_table::SPACES.iter().copied()), diff --git a/src/uu/tr/src/tr.rs b/src/uu/tr/src/tr.rs index 33f61d17fe6..58976d3ac06 100644 --- a/src/uu/tr/src/tr.rs +++ b/src/uu/tr/src/tr.rs @@ -148,9 +148,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("tr") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("tr")) .about(translate!("tr-about")) .override_usage(format_usage(&translate!("tr-usage"))) .after_help(translate!("tr-after-help")) diff --git a/src/uu/truncate/src/truncate.rs b/src/uu/truncate/src/truncate.rs index 6f460cfd708..31a67f5f82a 100644 --- a/src/uu/truncate/src/truncate.rs +++ b/src/uu/truncate/src/truncate.rs @@ -124,7 +124,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - let cmd = Command::new(uucore::util_name()) + let cmd = Command::new("truncate") .version(uucore::crate_version!()) .about(translate!("truncate-about")) .override_usage(format_usage(&translate!("truncate-usage"))) diff --git a/src/uu/tsort/Cargo.toml b/src/uu/tsort/Cargo.toml index 84a31349643..227cab634ea 100644 --- a/src/uu/tsort/Cargo.toml +++ b/src/uu/tsort/Cargo.toml @@ -28,7 +28,7 @@ uucore = { workspace = true } rustc-hash = { workspace = true } [target.'cfg(unix)'.dependencies] -nix = { workspace = true, features = ["fs"] } +rustix = { workspace = true, features = ["fs"] } [[bin]] name = "tsort" diff --git a/src/uu/tsort/src/tsort.rs b/src/uu/tsort/src/tsort.rs index a689cd05be4..c6a6829d67b 100644 --- a/src/uu/tsort/src/tsort.rs +++ b/src/uu/tsort/src/tsort.rs @@ -49,7 +49,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { translate!( "tsort-error-extra-operand", "operand" => extra.quote(), - "util" => uucore::util_name() + "util" => "tsort" ), )); } @@ -82,19 +82,18 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { target_os = "linux", target_os = "android", target_os = "fuchsia", - target_os = "wasi", target_env = "uclibc", target_os = "freebsd", ))] { - use nix::fcntl::{PosixFadviseAdvice, posix_fadvise}; + use rustix::fs::{Advice, fadvise}; use std::os::unix::io::AsFd; - posix_fadvise( + fadvise( file.as_fd(), - 0, // offset 0 => from the start of the file - 0, // length 0 => for the whole file - PosixFadviseAdvice::POSIX_FADV_SEQUENTIAL, + 0, // offset 0 => from the start of the file + None, // None => for the whole file + Advice::Sequential, ) .ok(); } @@ -108,9 +107,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("tsort") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("tsort")) .override_usage(format_usage(&translate!("tsort-usage"))) .about(translate!("tsort-about")) .infer_long_args(true) diff --git a/src/uu/tty/Cargo.toml b/src/uu/tty/Cargo.toml index fe5a97b07e7..6fe03e42525 100644 --- a/src/uu/tty/Cargo.toml +++ b/src/uu/tty/Cargo.toml @@ -24,7 +24,7 @@ uucore = { workspace = true, features = ["fs"] } fluent = { workspace = true } [target.'cfg(unix)'.dependencies] -nix = { workspace = true, features = ["term"] } +rustix = { workspace = true, features = ["fs", "termios"] } [[bin]] name = "tty" diff --git a/src/uu/tty/src/tty.rs b/src/uu/tty/src/tty.rs index cb5ae8e1721..edab189fd76 100644 --- a/src/uu/tty/src/tty.rs +++ b/src/uu/tty/src/tty.rs @@ -39,10 +39,12 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let mut stdout = std::io::stdout(); - let name = nix::unistd::ttyname(std::io::stdin()); + let name = rustix::termios::ttyname(std::io::stdin(), Vec::with_capacity(8)); let write_result = if let Ok(name) = name { - stdout.write_all_os(name.as_os_str()) + use std::os::unix::ffi::OsStrExt; + let os_name = std::ffi::OsStr::from_bytes(name.as_bytes()); + stdout.write_all_os(os_name) } else { set_exit_code(1); writeln!(stdout, "{}", translate!("tty-not-a-tty")) @@ -58,7 +60,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - let cmd = Command::new(uucore::util_name()) + let cmd = Command::new("tty") .version(uucore::crate_version!()) .about(translate!("tty-about")) .override_usage(format_usage(&translate!("tty-usage"))) diff --git a/src/uu/uname/src/uname.rs b/src/uu/uname/src/uname.rs index f83ef0d12b3..1439bd6f7f2 100644 --- a/src/uu/uname/src/uname.rs +++ b/src/uu/uname/src/uname.rs @@ -141,9 +141,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("uname") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("uname")) .about(translate!("uname-about")) .override_usage(format_usage(&translate!("uname-usage"))) .infer_long_args(true) diff --git a/src/uu/unexpand/src/unexpand.rs b/src/uu/unexpand/src/unexpand.rs index eae91164608..9c963f7aef3 100644 --- a/src/uu/unexpand/src/unexpand.rs +++ b/src/uu/unexpand/src/unexpand.rs @@ -235,7 +235,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("unexpand") .version(uucore::crate_version!()) .help_template(uucore::localized_help_template(uucore::util_name())) .override_usage(format_usage(&translate!("unexpand-usage"))) diff --git a/src/uu/uniq/src/uniq.rs b/src/uu/uniq/src/uniq.rs index 109e49d6224..e249aabe63c 100644 --- a/src/uu/uniq/src/uniq.rs +++ b/src/uu/uniq/src/uniq.rs @@ -655,7 +655,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { return Err(map_clap_errors(clap_error)); } // Use ErrorFormatter directly to handle error - let formatter = uucore::clap_localization::ErrorFormatter::new(uucore::util_name()); + let formatter = uucore::clap_localization::ErrorFormatter::new("uniq"); formatter.print_error_and_exit_with_callback(&clap_error, 1, || {}); } }; @@ -699,7 +699,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - let cmd = Command::new(uucore::util_name()) + let cmd = Command::new("uniq") .version(uucore::crate_version!()) .about(translate!("uniq-about")) .override_usage(format_usage(&translate!("uniq-usage"))) diff --git a/src/uu/unlink/src/unlink.rs b/src/uu/unlink/src/unlink.rs index caea413da51..188b6ad2d7f 100644 --- a/src/uu/unlink/src/unlink.rs +++ b/src/uu/unlink/src/unlink.rs @@ -27,9 +27,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("unlink") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("unlink")) .about(translate!("unlink-about")) .override_usage(format_usage(&translate!("unlink-usage"))) .infer_long_args(true) diff --git a/src/uu/uptime/src/uptime.rs b/src/uu/uptime/src/uptime.rs index 71ffd980d66..36c8760fd70 100644 --- a/src/uu/uptime/src/uptime.rs +++ b/src/uu/uptime/src/uptime.rs @@ -74,9 +74,9 @@ pub fn uu_app() -> Command { #[cfg(target_env = "musl")] let about = translate!("uptime-about") + &translate!("uptime-about-musl-warning"); - let cmd = Command::new(uucore::util_name()) + let cmd = Command::new("uptime") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("uptime")) .about(about) .override_usage(format_usage(&translate!("uptime-usage"))) .infer_long_args(true) diff --git a/src/uu/users/src/users.rs b/src/uu/users/src/users.rs index ea4b63d0d2a..5d78b17f9a7 100644 --- a/src/uu/users/src/users.rs +++ b/src/uu/users/src/users.rs @@ -86,9 +86,9 @@ pub fn uu_app() -> Command { #[cfg(target_env = "musl")] let about = translate!("users-about") + &translate!("users-about-musl-warning"); - Command::new(uucore::util_name()) + Command::new("users") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("users")) .about(about) .override_usage(format_usage(&translate!("users-usage"))) .infer_long_args(true) diff --git a/src/uu/wc/Cargo.toml b/src/uu/wc/Cargo.toml index b4c2005af2b..a285d58c8a9 100644 --- a/src/uu/wc/Cargo.toml +++ b/src/uu/wc/Cargo.toml @@ -33,7 +33,7 @@ unicode-width = { workspace = true } [target.'cfg(unix)'.dependencies] libc = { workspace = true } -nix = { workspace = true } +rustix = { workspace = true, features = ["fs"] } [dev-dependencies] divan = { workspace = true } diff --git a/src/uu/wc/benches/wc_bench.rs b/src/uu/wc/benches/wc_bench.rs index 67523f0bb67..79f8fbd2fea 100644 --- a/src/uu/wc/benches/wc_bench.rs +++ b/src/uu/wc/benches/wc_bench.rs @@ -8,7 +8,7 @@ use uu_wc::uumain; use uucore::benchmark::{create_test_file, run_util_function, text_data}; /// Benchmark different file sizes for byte counting -#[divan::bench(args = [500])] +#[divan::bench(args = [1, 500])] //todo: add 10kb to measure splice() overhead fn wc_bytes_synthetic(bencher: Bencher, size_mb: usize) { let temp_dir = tempfile::tempdir().unwrap(); let data = text_data::generate_by_size(size_mb, 80); diff --git a/src/uu/wc/src/count_fast.rs b/src/uu/wc/src/count_fast.rs index 5e2ecd080a0..637a777e4fe 100644 --- a/src/uu/wc/src/count_fast.rs +++ b/src/uu/wc/src/count_fast.rs @@ -9,15 +9,11 @@ use uucore::hardware::SimdPolicy; use super::WordCountable; -#[cfg(any(target_os = "linux", target_os = "android"))] -use std::fs::OpenOptions; use std::io::{self, ErrorKind, Read}; #[cfg(unix)] use libc::{_SC_PAGESIZE, S_IFREG, sysconf}; #[cfg(unix)] -use nix::sys::stat; -#[cfg(unix)] use std::io::{Seek, SeekFrom}; #[cfg(unix)] use std::os::fd::{AsFd, AsRawFd}; @@ -31,11 +27,9 @@ const FILE_ATTRIBUTE_NORMAL: u32 = 128; #[cfg(any(target_os = "linux", target_os = "android"))] use libc::S_IFIFO; #[cfg(any(target_os = "linux", target_os = "android"))] -use uucore::pipes::{pipe, splice, splice_exact}; +use uucore::pipes::{MAX_ROOTLESS_PIPE_SIZE, pipe, splice, splice_exact}; const BUF_SIZE: usize = 256 * 1024; -#[cfg(any(target_os = "linux", target_os = "android"))] -const SPLICE_SIZE: usize = 128 * 1024; /// This is a Linux-specific function to count the number of bytes using the /// `splice` system call, which is faster than using `read`. @@ -45,26 +39,19 @@ const SPLICE_SIZE: usize = 128 * 1024; #[inline] #[cfg(any(target_os = "linux", target_os = "android"))] fn count_bytes_using_splice(fd: &impl AsFd) -> Result { - let null_file = OpenOptions::new() - .write(true) - .open("/dev/null") - .map_err(|_| 0_usize)?; - let null_rdev = stat::fstat(null_file.as_fd()).map_err(|_| 0_usize)?.st_rdev as libc::dev_t; - if (libc::major(null_rdev), libc::minor(null_rdev)) != (1, 3) { - // This is not a proper /dev/null, writing to it is probably bad - // Bit of an edge case, but it has been known to happen - return Err(0); - } + let null_file = uucore::pipes::dev_null().ok_or(0_usize)?; + // todo: avoid generating broker if input is pipe (fcntl_setpipe_size succeed) and directly splice() to /dev/null to save RAM usage let (pipe_rd, pipe_wr) = pipe().map_err(|_| 0_usize)?; let mut byte_count = 0; + // improve throughput from pipe + let _ = rustix::pipe::fcntl_setpipe_size(fd, MAX_ROOTLESS_PIPE_SIZE); loop { - match splice(fd, &pipe_wr, SPLICE_SIZE) { + match splice(fd, &pipe_wr, MAX_ROOTLESS_PIPE_SIZE) { Ok(0) => break, Ok(res) => { byte_count += res; // Silent the warning as we want to the error message - #[allow(clippy::question_mark)] if splice_exact(&pipe_rd, &null_file, res).is_err() { return Err(byte_count); } @@ -92,7 +79,7 @@ pub(crate) fn count_bytes_fast(handle: &mut T) -> (usize, Opti #[cfg(unix)] { let fd = handle.as_fd(); - if let Ok(stat) = stat::fstat(fd) { + if let Ok(stat) = rustix::fs::fstat(fd) { // If the file is regular, then the `st_size` should hold // the file's size in bytes. // If stat.st_size = 0 then diff --git a/src/uu/wc/src/countable.rs b/src/uu/wc/src/countable.rs index 5b2ad7a9965..32d56553879 100644 --- a/src/uu/wc/src/countable.rs +++ b/src/uu/wc/src/countable.rs @@ -20,13 +20,20 @@ pub trait WordCountable: AsFd + AsRawFd + Read { fn inner_file(&mut self) -> Option<&mut File>; } -#[cfg(not(unix))] +#[cfg(all(not(unix), not(target_os = "wasi")))] pub trait WordCountable: Read { type Buffered: BufRead; fn buffered(self) -> Self::Buffered; fn inner_file(&mut self) -> Option<&mut File>; } +#[cfg(target_os = "wasi")] +pub trait WordCountable: Read { + type Buffered: BufRead; + fn buffered(self) -> Self::Buffered; +} + +#[cfg(not(target_os = "wasi"))] impl WordCountable for StdinLock<'_> { type Buffered = Self; @@ -38,6 +45,16 @@ impl WordCountable for StdinLock<'_> { } } +#[cfg(target_os = "wasi")] +impl WordCountable for StdinLock<'_> { + type Buffered = Self; + + fn buffered(self) -> Self::Buffered { + self + } +} + +#[cfg(not(target_os = "wasi"))] impl WordCountable for File { type Buffered = BufReader; @@ -49,3 +66,12 @@ impl WordCountable for File { Some(self) } } + +#[cfg(target_os = "wasi")] +impl WordCountable for File { + type Buffered = BufReader; + + fn buffered(self) -> Self::Buffered { + BufReader::new(self) + } +} diff --git a/src/uu/wc/src/wc.rs b/src/uu/wc/src/wc.rs index 0c09c2c3a43..90c80752f74 100644 --- a/src/uu/wc/src/wc.rs +++ b/src/uu/wc/src/wc.rs @@ -297,12 +297,11 @@ impl<'a> Input<'a> { #[cfg(unix)] fn is_stdin_small_file() -> bool { - use nix::sys::stat; use std::os::fd::AsFd; matches!( - stat::fstat(io::stdin().as_fd()), - Ok(meta) if meta.st_mode & libc::S_IFMT == libc::S_IFREG && meta.st_size <= (10 << 20) + rustix::fs::fstat(io::stdin().as_fd()), + Ok(meta) if meta.st_mode as libc::mode_t & libc::S_IFMT == libc::S_IFREG && meta.st_size <= (10 << 20) ) } @@ -393,7 +392,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("wc") .version(uucore::crate_version!()) .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("wc-about")) diff --git a/src/uu/who/locales/en-US.ftl b/src/uu/who/locales/en-US.ftl index f092489a16a..2984c2fdcc5 100644 --- a/src/uu/who/locales/en-US.ftl +++ b/src/uu/who/locales/en-US.ftl @@ -32,9 +32,7 @@ who-user-count = # { $count -> } # Idle time indicators -who-idle-current = . who-idle-old = old -who-idle-unknown = ? # System information who-runlevel = run-level { $level } diff --git a/src/uu/who/locales/fr-FR.ftl b/src/uu/who/locales/fr-FR.ftl index ca88a4946c1..0c853e51ee9 100644 --- a/src/uu/who/locales/fr-FR.ftl +++ b/src/uu/who/locales/fr-FR.ftl @@ -32,7 +32,6 @@ who-user-count = # { $count -> # Idle time indicators who-idle-old = anc. -who-idle-unknown = ? # System information who-runlevel = niveau-exec { $level } @@ -56,4 +55,4 @@ who-heading-exit = SORTIE who-canonicalize-error = échec de canonicalisation de { $host } # Platform-specific messages -who-unsupported-openbsd = commande non supportée sur OpenBSD +who-unsupported-openbsd = commande non prise en charge sur OpenBSD diff --git a/src/uu/who/src/platform/unix.rs b/src/uu/who/src/platform/unix.rs index a91f12fdeb8..975ac31c4df 100644 --- a/src/uu/who/src/platform/unix.rs +++ b/src/uu/who/src/platform/unix.rs @@ -179,16 +179,14 @@ fn time_string(ut: &UtmpxRecord) -> String { #[inline] fn current_tty() -> String { - unsafe { - let res = ttyname(STDIN_FILENO); - if res.is_null() { - String::new() - } else { - CStr::from_ptr(res.cast_const()) - .to_string_lossy() - .trim_start_matches("/dev/") - .to_owned() - } + let p = unsafe { ttyname(STDIN_FILENO) }; + if p.is_null() { + String::new() + } else { + unsafe { CStr::from_ptr(p) } + .to_string_lossy() + .trim_start_matches("/dev/") + .to_owned() } } @@ -381,7 +379,7 @@ impl Who { } let idle = if last_change == 0 { - translate!("who-idle-unknown").into() + " ?".into() } else { idle_string(last_change, 0) }; diff --git a/src/uu/who/src/who.rs b/src/uu/who/src/who.rs index 197fbe71287..e82990b7924 100644 --- a/src/uu/who/src/who.rs +++ b/src/uu/who/src/who.rs @@ -45,7 +45,7 @@ pub fn uu_app() -> Command { #[cfg(target_env = "musl")] let about = translate!("who-about") + &translate!("who-about-musl-warning"); - let cmd = Command::new(uucore::util_name()) + let cmd = Command::new("who") .version(uucore::crate_version!()) .about(about) .override_usage(format_usage(&translate!("who-usage"))) diff --git a/src/uu/whoami/src/whoami.rs b/src/uu/whoami/src/whoami.rs index 3851c10748d..17f50d1a62f 100644 --- a/src/uu/whoami/src/whoami.rs +++ b/src/uu/whoami/src/whoami.rs @@ -25,9 +25,9 @@ pub fn whoami() -> UResult { } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("whoami") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("whoami")) .about(translate!("whoami-about")) .override_usage(translate!("whoami-usage")) .infer_long_args(true) diff --git a/src/uu/yes/Cargo.toml b/src/uu/yes/Cargo.toml index b8703bb55d1..1c2c53dde43 100644 --- a/src/uu/yes/Cargo.toml +++ b/src/uu/yes/Cargo.toml @@ -22,12 +22,7 @@ path = "src/yes.rs" clap = { workspace = true } itertools = { workspace = true } fluent = { workspace = true } - -[target.'cfg(unix)'.dependencies] -uucore = { workspace = true, features = ["pipes", "signals"] } - -[target.'cfg(not(unix))'.dependencies] -uucore = { workspace = true, features = ["pipes"] } +uucore = { workspace = true } [[bin]] name = "yes" diff --git a/src/uu/yes/src/yes.rs b/src/uu/yes/src/yes.rs index 88d2c7ed113..e7132959580 100644 --- a/src/uu/yes/src/yes.rs +++ b/src/uu/yes/src/yes.rs @@ -9,7 +9,7 @@ use clap::{Arg, ArgAction, Command, builder::ValueParser}; use std::error::Error; use std::ffi::OsString; use std::io::{self, Write}; -use uucore::error::{UResult, USimpleError}; +use uucore::error::{UResult, USimpleError, strip_errno}; use uucore::format_usage; use uucore::translate; @@ -33,15 +33,15 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { Err(err) if err.kind() == io::ErrorKind::BrokenPipe => Ok(()), Err(err) => Err(USimpleError::new( 1, - translate!("yes-error-standard-output", "error" => err), + translate!("yes-error-standard-output", "error" => strip_errno(&err)), )), } } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) + Command::new("yes") .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) + .help_template(uucore::localized_help_template("yes")) .about(translate!("yes-about")) .override_usage(format_usage(&translate!("yes-usage"))) .arg( @@ -92,14 +92,9 @@ fn args_into_buffer<'a>( /// Assumes buf holds a single output line forged from the command line arguments, copies it /// repeatedly until the buffer holds as many copies as it can under [`BUF_SIZE`]. fn prepare_buffer(buf: &mut Vec) { - if buf.len() * 2 > BUF_SIZE { - return; - } - - assert!(!buf.is_empty()); - let line_len = buf.len(); - let target_size = line_len * (BUF_SIZE / line_len); + debug_assert!(line_len > 0, "buffer is not empty since we have newline"); + let target_size = line_len * (BUF_SIZE / line_len); // 0 if line_len is already large enough while buf.len() < target_size { let to_copy = std::cmp::min(target_size - buf.len(), buf.len()); diff --git a/src/uucore/Cargo.toml b/src/uucore/Cargo.toml index 2f11ecfd30a..bd4acb8b2fa 100644 --- a/src/uucore/Cargo.toml +++ b/src/uucore/Cargo.toml @@ -37,6 +37,7 @@ jiff = { workspace = true, optional = true, features = [ "tzdb-concatenated", ] } rustc-hash = { workspace = true } +rustix = { workspace = true, features = ["fs", "pipe"] } time = { workspace = true, optional = true, features = [ "formatting", "local-offset", diff --git a/src/uucore/build.rs b/src/uucore/build.rs index 15068f28aab..f3b4df331a7 100644 --- a/src/uucore/build.rs +++ b/src/uucore/build.rs @@ -347,6 +347,47 @@ fn embed_locale_file( /// /// Returns an error if `for_each_locale` fails, which typically happens if /// reading a locale file or writing to the `embedded_file` fails. +/// Check if we are cross-compiling for WASI (build.rs runs on the host, +/// so `#[cfg(target_os = "wasi")]` does not work here). +fn is_wasi_target() -> bool { + env::var("CARGO_CFG_TARGET_OS") + .map(|os| os == "wasi") + .unwrap_or(false) +} + +/// For WASI/WASM builds, embed ALL available .ftl files in a locale +/// directory so the playground can switch languages at runtime. +fn embed_all_locales_for_component( + embedded_file: &mut File, + component_name: &str, + path_builder: &F, +) -> Result<(), Box> +where + F: Fn(&str) -> PathBuf, +{ + let en_path = path_builder("en-US"); + if let Some(locale_dir) = en_path.parent() { + if locale_dir.exists() { + for entry in std::fs::read_dir(locale_dir)? { + let entry = entry?; + let path = entry.path(); + if path.extension().is_some_and(|e| e == "ftl") { + if let Some(locale) = path.file_stem().and_then(|s| s.to_str()) { + embed_locale_file( + embedded_file, + &path, + &format!("{component_name}/{locale}.ftl"), + locale, + component_name, + )?; + } + } + } + } + } + Ok(()) +} + fn embed_component_locales( embedded_file: &mut File, locales: &(String, Option), @@ -356,6 +397,10 @@ fn embed_component_locales( where F: Fn(&str) -> PathBuf, { + if is_wasi_target() { + return embed_all_locales_for_component(embedded_file, component_name, &path_builder); + } + for_each_locale(locales, |locale| { let locale_path = path_builder(locale); embed_locale_file( diff --git a/src/uucore/locales/en-US.ftl b/src/uucore/locales/en-US.ftl index 15896209526..93a76157768 100644 --- a/src/uucore/locales/en-US.ftl +++ b/src/uucore/locales/en-US.ftl @@ -80,3 +80,7 @@ checksum-failed-open-file = { $count -> *[other] { $count } listed files could not be read } checksum-error-algo-bad-format = { $file }: { $line }: improperly formatted { $algo } checksum line + +# uudoc tldr examples messages +uudoc-tldr-attribution = The examples are provided by the [tldr-pages project](https://tldr.sh) under the [CC BY 4.0 License](https://github.com/tldr-pages/tldr/blob/main/LICENSE.md). +uudoc-tldr-disclaimer = Please note that, as uutils is a work in progress, some examples might fail. diff --git a/src/uucore/locales/fr-FR.ftl b/src/uucore/locales/fr-FR.ftl index e9ff4abe475..98a18fbce39 100644 --- a/src/uucore/locales/fr-FR.ftl +++ b/src/uucore/locales/fr-FR.ftl @@ -74,3 +74,7 @@ checksum-failed-open-file = { $count -> *[other] { $count } fichiers passés n'ont pas pu être lu } checksum-error-algo-bad-format = { $file }: { $line }: ligne invalide pour { $algo } + +# Messages uudoc pour les exemples tldr +uudoc-tldr-attribution = Les exemples sont fournis par le [projet tldr-pages](https://tldr.sh) sous la [licence CC BY 4.0](https://github.com/tldr-pages/tldr/blob/main/LICENSE.md). +uudoc-tldr-disclaimer = Veuillez noter que, uutils étant en cours de développement, certains exemples peuvent échouer. diff --git a/src/uucore/src/lib/features/checksum/mod.rs b/src/uucore/src/lib/features/checksum/mod.rs index 1f77b7e635f..d18e2adaad8 100644 --- a/src/uucore/src/lib/features/checksum/mod.rs +++ b/src/uucore/src/lib/features/checksum/mod.rs @@ -115,6 +115,8 @@ impl AlgoKind { ALGORITHM_OPTIONS_SHA384 => Sha384, ALGORITHM_OPTIONS_SHA512 => Sha512, + // Extensions not in GNU as of version 9.10 + ALGORITHM_OPTIONS_BLAKE3 => Blake3, ALGORITHM_OPTIONS_SHAKE128 => Shake128, ALGORITHM_OPTIONS_SHAKE256 => Shake256, _ => return Err(ChecksumError::UnknownAlgorithm(algo.as_ref().to_string()).into()), @@ -198,6 +200,22 @@ impl AlgoKind { use AlgoKind::*; matches!(self, Sysv | Bsd | Crc | Crc32b) } + + /// When checking untagged format lines, non-XOF non-legacy algorithms + /// should report "improperly formatted lines" if the digest length isn't + /// equivalent to this. + pub fn expected_digest_bit_len(self) -> Option { + match self { + Self::Md5 => Some(Md5::BIT_SIZE), + Self::Sm3 => Some(Sm3::BIT_SIZE), + Self::Sha1 => Some(Sha1::BIT_SIZE), + Self::Sha224 => Some(Sha224::BIT_SIZE), + Self::Sha256 => Some(Sha256::BIT_SIZE), + Self::Sha384 => Some(Sha384::BIT_SIZE), + Self::Sha512 => Some(Sha512::BIT_SIZE), + _ => None, + } + } } /// Holds a length for a SHA2 of SHA3 algorithm kind. @@ -245,11 +263,11 @@ pub enum SizedAlgoKind { Md5, Sm3, Sha1, - Blake3, Sha2(ShaLength), Sha3(ShaLength), - // Note: we store Blake2b's length as BYTES. - Blake2b(Option), + // Note: we store Blake*'s length as BYTES. + Blake2b(usize), + Blake3(usize), // Shake* length are stored in bits. Shake128(Option), Shake256(Option), @@ -267,7 +285,6 @@ impl SizedAlgoKind { | ak::Md5 | ak::Sm3 | ak::Sha1 - | ak::Blake3 | ak::Sha224 | ak::Sha256 | ak::Sha384 @@ -282,8 +299,9 @@ impl SizedAlgoKind { (ak::Md5, _) => Ok(Self::Md5), (ak::Sm3, _) => Ok(Self::Sm3), (ak::Sha1, _) => Ok(Self::Sha1), - (ak::Blake3, _) => Ok(Self::Blake3), + (ak::Blake2b, l) => Ok(Self::Blake2b(l.unwrap_or(Blake2b::DEFAULT_BYTE_SIZE))), + (ak::Blake3, l) => Ok(Self::Blake3(l.unwrap_or(Blake3::DEFAULT_BYTE_SIZE))), (ak::Shake128, l) => Ok(Self::Shake128(l)), (ak::Shake256, l) => Ok(Self::Shake256(l)), (ak::Sha2, Some(l)) => Ok(Self::Sha2(ShaLength::try_from(l)?)), @@ -291,13 +309,6 @@ impl SizedAlgoKind { (algo @ (ak::Sha2 | ak::Sha3), None) => { Err(ChecksumError::LengthRequiredForSha(algo.to_lowercase().into()).into()) } - // [`calculate_blake2b_length`] expects a length in bits but we - // have a length in bytes. - (ak::Blake2b, Some(l)) => Ok(Self::Blake2b(calculate_blake2b_length_str( - &(8 * l).to_string(), - )?)), - (ak::Blake2b, None) => Ok(Self::Blake2b(None)), - (ak::Sha224, None) => Ok(Self::Sha2(ShaLength::Len224)), (ak::Sha256, None) => Ok(Self::Sha2(ShaLength::Len256)), (ak::Sha384, None) => Ok(Self::Sha2(ShaLength::Len384)), @@ -310,11 +321,11 @@ impl SizedAlgoKind { Self::Md5 => "MD5".into(), Self::Sm3 => "SM3".into(), Self::Sha1 => "SHA1".into(), - Self::Blake3 => "BLAKE3".into(), Self::Sha2(len) => format!("SHA{}", len.as_usize()), Self::Sha3(len) => format!("SHA3-{}", len.as_usize()), - Self::Blake2b(Some(byte_len)) => format!("BLAKE2b-{}", byte_len * 8), - Self::Blake2b(None) => "BLAKE2b".into(), + Self::Blake2b(Blake2b::DEFAULT_BYTE_SIZE) => "BLAKE2b".into(), + Self::Blake2b(byte_len) => format!("BLAKE2b-{}", byte_len * 8), + Self::Blake3(byte_len) => format!("BLAKE3-{}", byte_len * 8), Self::Shake128(opt_bit_len) => format!( "SHAKE128-{}", opt_bit_len.unwrap_or(Shake128::DEFAULT_BIT_SIZE) @@ -339,7 +350,6 @@ impl SizedAlgoKind { Self::Md5 => Box::new(Md5::default()), Self::Sm3 => Box::new(Sm3::default()), Self::Sha1 => Box::new(Sha1::default()), - Self::Blake3 => Box::new(Blake3::default()), Self::Sha2(Len224) => Box::new(Sha224::default()), Self::Sha2(Len256) => Box::new(Sha256::default()), Self::Sha2(Len384) => Box::new(Sha384::default()), @@ -348,9 +358,8 @@ impl SizedAlgoKind { Self::Sha3(Len256) => Box::new(Sha3_256::default()), Self::Sha3(Len384) => Box::new(Sha3_384::default()), Self::Sha3(Len512) => Box::new(Sha3_512::default()), - Self::Blake2b(len_opt) => { - Box::new(len_opt.map(Blake2b::with_output_bytes).unwrap_or_default()) - } + Self::Blake2b(len) => Box::new(Blake2b::with_output_bytes(*len)), + Self::Blake3(len) => Box::new(Blake3::with_output_bytes(*len)), Self::Shake128(len_opt) => { Box::new(len_opt.map(Shake128::with_output_bits).unwrap_or_default()) } @@ -369,10 +378,10 @@ impl SizedAlgoKind { Self::Md5 => 128, Self::Sm3 => 512, Self::Sha1 => 160, - Self::Blake3 => 256, Self::Sha2(len) => len.as_usize(), Self::Sha3(len) => len.as_usize(), - Self::Blake2b(len) => len.unwrap_or(Blake2b::DEFAULT_BYTE_SIZE * 8), + Self::Blake2b(len) => len * 8, + Self::Blake3(len) => len * 8, Self::Shake128(len) => len.unwrap_or(Shake128::DEFAULT_BIT_SIZE), Self::Shake256(len) => len.unwrap_or(Shake256::DEFAULT_BIT_SIZE), } @@ -486,35 +495,57 @@ pub fn digest_reader( Ok((digest.result(), output_size)) } -/// Calculates the length of the digest. -pub fn calculate_blake2b_length_str(bit_length: &str) -> UResult> { - // Blake2b's length is parsed in an u64. - match bit_length.parse::() { - Ok(0) => Ok(None), +pub enum BlakeLength<'s> { + Int(usize), + String(&'s str), +} - // Error cases - Ok(n) if n > 512 => { - show_error!("{}", ChecksumError::InvalidLength(bit_length.into())); - Err(ChecksumError::LengthTooBigForBlake("BLAKE2b".into()).into()) - } - Err(e) if *e.kind() == IntErrorKind::PosOverflow => { - show_error!("{}", ChecksumError::InvalidLength(bit_length.into())); - Err(ChecksumError::LengthTooBigForBlake("BLAKE2b".into()).into()) +/// Expects a size in BITS, either as a string or int, and returns it as a BYTE +/// length. +/// +/// Note: when the input is a string, validation may print error messages. +/// Note: when the algo is Blake2b, values that are above 512 +/// (Blake2b::DEFAULT_BIT_SIZE) are errors. +pub fn parse_blake_length(algo: AlgoKind, bit_length: BlakeLength<'_>) -> UResult { + debug_assert!(matches!(algo, AlgoKind::Blake2b | AlgoKind::Blake3)); + + let print_error = || { + if let BlakeLength::String(s) = bit_length { + show_error!("{}", ChecksumError::InvalidLength(s.to_string())); } - Err(_) => Err(ChecksumError::InvalidLength(bit_length.into()).into()), + }; - Ok(n) if n % 8 != 0 => { - show_error!("{}", ChecksumError::InvalidLength(bit_length.into())); - Err(ChecksumError::LengthNotMultipleOf8.into()) - } + let n = match bit_length { + BlakeLength::Int(i) => i, + BlakeLength::String(s) => s.parse::().map_err(|e| { + if *e.kind() == IntErrorKind::PosOverflow { + print_error(); + ChecksumError::LengthTooBigForBlake(algo.to_uppercase().into()) + } else { + ChecksumError::InvalidLength(s.to_string()) + } + })?, + }; - // Valid cases + if n == 0 { + return Ok(match algo { + AlgoKind::Blake2b => Blake2b::DEFAULT_BYTE_SIZE, + AlgoKind::Blake3 => Blake3::DEFAULT_BYTE_SIZE, + _ => unreachable!(), + }); + } + + if algo == AlgoKind::Blake2b && n > Blake2b::DEFAULT_BIT_SIZE { + print_error(); + return Err(ChecksumError::LengthTooBigForBlake(algo.to_uppercase().into()).into()); + } - // When length is 512, it is blake2b's default. So, don't show it - Ok(512) => Ok(None), - // Divide by 8, as our blake2b implementation expects bytes instead of bits. - Ok(n) => Ok(Some(n / 8)), + if n % 8 != 0 { + print_error(); + return Err(ChecksumError::LengthNotMultipleOf8.into()); } + + Ok(n / 8) } pub fn validate_sha2_sha3_length(algo_name: AlgoKind, length: Option) -> UResult { @@ -632,10 +663,19 @@ mod tests { #[test] fn test_calculate_blake2b_length() { - assert_eq!(calculate_blake2b_length_str("0").unwrap(), None); - assert!(calculate_blake2b_length_str("10").is_err()); - assert!(calculate_blake2b_length_str("520").is_err()); - assert_eq!(calculate_blake2b_length_str("512").unwrap(), None); - assert_eq!(calculate_blake2b_length_str("256").unwrap(), Some(32)); + assert_eq!( + parse_blake_length(AlgoKind::Blake2b, BlakeLength::String("0")).unwrap(), + Blake2b::DEFAULT_BYTE_SIZE + ); + assert!(parse_blake_length(AlgoKind::Blake2b, BlakeLength::String("10")).is_err()); + assert!(parse_blake_length(AlgoKind::Blake2b, BlakeLength::String("520")).is_err()); + assert_eq!( + parse_blake_length(AlgoKind::Blake2b, BlakeLength::String("512")).unwrap(), + Blake2b::DEFAULT_BYTE_SIZE + ); + assert_eq!( + parse_blake_length(AlgoKind::Blake2b, BlakeLength::String("256")).unwrap(), + 32 + ); } } diff --git a/src/uucore/src/lib/features/checksum/validate.rs b/src/uucore/src/lib/features/checksum/validate.rs index 17b1c7bf522..bb47ff6c920 100644 --- a/src/uucore/src/lib/features/checksum/validate.rs +++ b/src/uucore/src/lib/features/checksum/validate.rs @@ -15,11 +15,12 @@ use std::io::{self, BufReader, Read, Write, stderr, stdin}; use os_display::Quotable; use crate::checksum::{ - AlgoKind, ChecksumError, ReadingMode, SizedAlgoKind, digest_reader, unescape_filename, + AlgoKind, BlakeLength, ChecksumError, ReadingMode, ShaLength, SizedAlgoKind, digest_reader, + parse_blake_length, unescape_filename, }; use crate::error::{FromIo, UError, UIoError, UResult, USimpleError}; use crate::quoting_style::{QuotingStyle, locale_aware_escape_name}; -use crate::sum::{self, DigestOutput}; +use crate::sum::{self, Blake2b, Blake3, DigestOutput}; use crate::{ os_str_as_bytes, os_str_from_bytes, read_os_string_lines, show, show_warning_caps, translate, }; @@ -489,7 +490,7 @@ impl LineInfo { } /// Extract the expected digest from the checksum string and decode it -fn get_raw_expected_digest(checksum: &str, byte_len_hint: Option) -> Option> { +fn get_raw_expected_digest(checksum: &str, bit_len_hint: Option) -> Option> { // If the length of the digest is not a multiple of 2, then it must be // improperly formatted (1 byte is 2 hex digits, and base64 strings should // always be a multiple of 4). @@ -497,6 +498,8 @@ fn get_raw_expected_digest(checksum: &str, byte_len_hint: Option) -> Opti return None; } + let byte_len_hint = bit_len_hint.map(|n| n.div_ceil(8)); + let checks_hint = |len| byte_len_hint.is_none_or(|hint| hint == len); // If the length of the string matches the one to be expected (in case it's @@ -614,6 +617,7 @@ fn identify_algo_name_and_length( algo_name_input: Option, last_algo: &mut Option, ) -> Result<(AlgoKind, Option), LineCheckError> { + use AlgoKind as ak; let algo_from_line = line_info.algo_name.clone().unwrap_or_default(); let Ok(line_algo) = AlgoKind::from_cksum(algo_from_line.to_lowercase()) else { // Unknown algorithm @@ -629,24 +633,25 @@ fn identify_algo_name_and_length( match (algo_name_input, line_algo) { (l, r) if l == r => (), // Edge case for SHA2, which matches SHA(224|256|384|512) - ( - AlgoKind::Sha2, - AlgoKind::Sha224 | AlgoKind::Sha256 | AlgoKind::Sha384 | AlgoKind::Sha512, - ) => (), + (ak::Sha2, ak::Sha224 | ak::Sha256 | ak::Sha384 | ak::Sha512) => (), _ => return Err(LineCheckError::ImproperlyFormatted), } } let bytes = if let Some(bitlen) = line_info.algo_bit_len { match line_algo { - AlgoKind::Blake2b if bitlen % 8 == 0 => Some(bitlen / 8), - AlgoKind::Sha2 | AlgoKind::Sha3 if [224, 256, 384, 512].contains(&bitlen) => { - Some(bitlen) + algo @ (ak::Blake2b | ak::Blake3) => { + match parse_blake_length(algo, BlakeLength::Int(bitlen)) { + Ok(len) => Some(len), + Err(_) => return Err(LineCheckError::ImproperlyFormatted), + } } - AlgoKind::Shake128 | AlgoKind::Shake256 => Some(bitlen), + ak::Sha2 | ak::Sha3 if [224, 256, 384, 512].contains(&bitlen) => Some(bitlen), + ak::Shake128 | ak::Shake256 => Some(bitlen), // Either - // the algo based line is provided with a bit length - // with an algorithm that does not support it (only Blake2B does). + // the algo based line is provided with a bit length with an + // algorithm that does not support it (only Blake2b, Blake3, sha2, + // and sha3 do). // // eg: MD5-128 (foo.txt) = fffffffff // ^ This is illegal @@ -654,9 +659,12 @@ fn identify_algo_name_and_length( // the given length is wrong because it's not a multiple of 8. _ => return Err(LineCheckError::ImproperlyFormatted), } - } else if line_algo == AlgoKind::Blake2b { + } else if line_algo == ak::Blake2b { // Default length with BLAKE2b, - Some(64) + Some(Blake2b::DEFAULT_BYTE_SIZE) + } else if line_algo == ak::Blake3 { + // Default length with BLAKE3, + Some(Blake3::DEFAULT_BYTE_SIZE) } else { None }; @@ -735,23 +743,23 @@ fn process_algo_based_line( ) -> Result<(), LineCheckError> { let filename_to_check = line_info.filename.as_slice(); - let (algo_kind, algo_byte_len) = - identify_algo_name_and_length(line_info, cli_algo_kind, last_algo)?; + let (algo_kind, algo_len) = identify_algo_name_and_length(line_info, cli_algo_kind, last_algo)?; // If the digest bitlen is known, we can check the format of the expected // checksum with it. - let digest_char_length_hint = match (algo_kind, algo_byte_len) { - (AlgoKind::Blake2b, Some(byte_len)) => Some(byte_len), - (AlgoKind::Shake128 | AlgoKind::Shake256, Some(bit_len)) => Some(bit_len.div_ceil(8)), - (AlgoKind::Shake128, None) => Some(sum::Shake128::DEFAULT_BIT_SIZE.div_ceil(8)), - (AlgoKind::Shake256, None) => Some(sum::Shake256::DEFAULT_BIT_SIZE.div_ceil(8)), + let digest_bit_length_hint = match (algo_kind, algo_len) { + (AlgoKind::Blake2b | AlgoKind::Blake3, Some(byte_len)) => Some(byte_len * 8), + (AlgoKind::Shake128 | AlgoKind::Shake256, Some(bit_len)) => Some(bit_len), + (AlgoKind::Shake128, None) => Some(sum::Shake128::DEFAULT_BIT_SIZE), + (AlgoKind::Shake256, None) => Some(sum::Shake256::DEFAULT_BIT_SIZE), _ => None, }; - let expected_checksum = get_raw_expected_digest(&line_info.checksum, digest_char_length_hint) + let expected_checksum = get_raw_expected_digest(&line_info.checksum, digest_bit_length_hint) .ok_or(LineCheckError::ImproperlyFormatted)?; - let algo = SizedAlgoKind::from_unsized(algo_kind, algo_byte_len)?; + let algo = SizedAlgoKind::from_unsized(algo_kind, algo_len) + .map_err(|_| LineCheckError::ImproperlyFormatted)?; compute_and_check_digest_from_file(filename_to_check, &expected_checksum, algo, opts) } @@ -764,6 +772,7 @@ fn process_non_algo_based_line( cli_algo_length: Option, opts: ChecksumValidateOptions, ) -> Result<(), LineCheckError> { + use AlgoKind as ak; let mut filename_to_check = line_info.filename.as_slice(); if filename_to_check.starts_with(b"*") && line_number == 0 @@ -772,22 +781,28 @@ fn process_non_algo_based_line( // Remove the leading asterisk if present - only for the first line filename_to_check = &filename_to_check[1..]; } - let expected_checksum = get_raw_expected_digest(&line_info.checksum, None) + + let expected_digest_sum = cli_algo_kind.expected_digest_bit_len(); + let expected_checksum = get_raw_expected_digest(&line_info.checksum, expected_digest_sum) .ok_or(LineCheckError::ImproperlyFormatted)?; // When a specific algorithm name is input, use it and use the provided // bits except when dealing with blake2b, sha2 and sha3, where we will // detect the length. - let (algo_kind, algo_byte_len) = match cli_algo_kind { - AlgoKind::Blake2b => (AlgoKind::Blake2b, Some(expected_checksum.len())), - algo @ (AlgoKind::Sha2 | AlgoKind::Sha3) => { + let algo_byte_len = match cli_algo_kind { + ak::Blake2b | ak::Blake3 => Some(expected_checksum.len()), + ak::Sha2 | ak::Sha3 => { // multiplication by 8 to get the number of bits - (algo, Some(expected_checksum.len() * 8)) + Some( + ShaLength::try_from(expected_checksum.len() * 8) + .map_err(|_| LineCheckError::ImproperlyFormatted)? + .as_usize(), + ) } - _ => (cli_algo_kind, cli_algo_length), + _ => cli_algo_length, }; - let algo = SizedAlgoKind::from_unsized(algo_kind, algo_byte_len)?; + let algo = SizedAlgoKind::from_unsized(cli_algo_kind, algo_byte_len)?; compute_and_check_digest_from_file(filename_to_check, &expected_checksum, algo, opts) } diff --git a/src/uucore/src/lib/features/fs.rs b/src/uucore/src/lib/features/fs.rs index 5e10f2ea513..590d0076fed 100644 --- a/src/uucore/src/lib/features/fs.rs +++ b/src/uucore/src/lib/features/fs.rs @@ -8,11 +8,7 @@ // spell-checker:ignore backport #[cfg(unix)] -use libc::{ - S_IFBLK, S_IFCHR, S_IFDIR, S_IFIFO, S_IFLNK, S_IFMT, S_IFREG, S_IFSOCK, S_IRGRP, S_IROTH, - S_IRUSR, S_ISGID, S_ISUID, S_ISVTX, S_IWGRP, S_IWOTH, S_IWUSR, S_IXGRP, S_IXOTH, S_IXUSR, - mkfifo, mode_t, -}; +use libc::mkfifo; #[cfg(all(unix, not(target_os = "redox")))] pub use libc::{major, makedev, minor}; use std::collections::HashSet; @@ -49,6 +45,8 @@ macro_rules! has { pub struct FileInformation( #[cfg(unix)] nix::sys::stat::FileStat, #[cfg(windows)] winapi_util::file::Information, + // WASI does not have nix::sys::stat, so we store std::fs::Metadata instead. + #[cfg(target_os = "wasi")] fs::Metadata, ); impl FileInformation { @@ -95,6 +93,16 @@ impl FileInformation { let file = open_options.read(true).open(path.as_ref())?; Self::from_file(&file) } + // WASI: use std::fs::metadata / symlink_metadata since nix is not available + #[cfg(target_os = "wasi")] + { + let metadata = if dereference { + fs::metadata(path.as_ref()) + } else { + fs::symlink_metadata(path.as_ref()) + }; + Ok(Self(metadata?)) + } } pub fn file_size(&self) -> u64 { @@ -107,6 +115,10 @@ impl FileInformation { { self.0.file_size() } + #[cfg(target_os = "wasi")] + { + self.0.len() + } } #[cfg(windows)] @@ -157,6 +169,9 @@ impl FileInformation { return self.0.st_nlink.try_into().unwrap(); #[cfg(windows)] return self.0.number_of_links(); + // WASI: nlink is not available in std::fs::Metadata, return 1 + #[cfg(target_os = "wasi")] + return 1; } #[cfg(unix)] @@ -176,6 +191,15 @@ impl PartialEq for FileInformation { } } +// WASI: compare by file type and size as a basic heuristic since +// device/inode numbers are not available through std::fs::Metadata. +#[cfg(target_os = "wasi")] +impl PartialEq for FileInformation { + fn eq(&self, other: &Self) -> bool { + self.0.file_type() == other.0.file_type() && self.0.len() == other.0.len() + } +} + #[cfg(target_os = "windows")] impl PartialEq for FileInformation { fn eq(&self, other: &Self) -> bool { @@ -198,6 +222,11 @@ impl Hash for FileInformation { self.0.volume_serial_number().hash(state); self.0.file_index().hash(state); } + #[cfg(target_os = "wasi")] + { + self.0.len().hash(state); + self.0.file_type().is_dir().hash(state); + } } } @@ -464,12 +493,44 @@ pub fn display_permissions(metadata: &fs::Metadata, display_file_type: bool) -> #[cfg(unix)] /// Display the permissions of a file pub fn display_permissions(metadata: &fs::Metadata, display_file_type: bool) -> String { - let mode: mode_t = metadata.mode() as mode_t; - display_permissions_unix(mode, display_file_type) + display_permissions_unix(metadata.mode(), display_file_type) +} + +/// Portable file mode bit constants, equivalent to the POSIX `S_I*` values. +/// +/// These are defined here as plain `u32` so they are available on every +/// platform, including Windows, without requiring `libc` or a Unix target. +/// Callers that previously used `libc::S_IFDIR` etc. can import these instead. +pub mod mode { + // File-type mask and values + pub const S_IFMT: u32 = 0o170_000; // bitmask for the file-type field + pub const S_IFSOCK: u32 = 0o140_000; // socket + pub const S_IFLNK: u32 = 0o120_000; // symbolic link + pub const S_IFREG: u32 = 0o100_000; // regular file + pub const S_IFBLK: u32 = 0o060_000; // block device + pub const S_IFDIR: u32 = 0o040_000; // directory + pub const S_IFCHR: u32 = 0o020_000; // character device + pub const S_IFIFO: u32 = 0o010_000; // named pipe (FIFO) + + // Permission and special-mode bits + pub const S_ISUID: u32 = 0o4000; // setuid + pub const S_ISGID: u32 = 0o2000; // setgid + pub const S_ISVTX: u32 = 0o1000; // sticky + + pub const S_IRUSR: u32 = 0o0400; // owner read + pub const S_IWUSR: u32 = 0o0200; // owner write + pub const S_IXUSR: u32 = 0o0100; // owner execute + + pub const S_IRGRP: u32 = 0o0040; // group read + pub const S_IWGRP: u32 = 0o0020; // group write + pub const S_IXGRP: u32 = 0o0010; // group execute + + pub const S_IROTH: u32 = 0o0004; // other read + pub const S_IWOTH: u32 = 0o0002; // other write + pub const S_IXOTH: u32 = 0o0001; // other execute } /// Returns a character representation of the file type based on its mode. -/// This function is specific to Unix-like systems. /// /// - `mode`: The mode of the file, typically obtained from file metadata. /// @@ -482,8 +543,8 @@ pub fn display_permissions(metadata: &fs::Metadata, display_file_type: bool) -> /// - 'l' for symbolic links /// - 's' for sockets /// - '?' for any other unrecognized file types -#[cfg(unix)] -fn get_file_display(mode: mode_t) -> char { +fn get_file_display(mode: u32) -> char { + use mode::{S_IFBLK, S_IFCHR, S_IFDIR, S_IFIFO, S_IFLNK, S_IFMT, S_IFREG, S_IFSOCK}; match mode & S_IFMT { S_IFDIR => 'd', S_IFCHR => 'c', @@ -500,9 +561,12 @@ fn get_file_display(mode: mode_t) -> char { // The logic below is more readable written this way. #[allow(clippy::if_not_else)] #[allow(clippy::cognitive_complexity)] -#[cfg(unix)] -/// Display the permissions of a file on a unix like system -pub fn display_permissions_unix(mode: mode_t, display_file_type: bool) -> String { +/// Display the unix permissions of a file +pub fn display_permissions_unix(mode: u32, display_file_type: bool) -> String { + use mode::{ + S_IRGRP, S_IROTH, S_IRUSR, S_ISGID, S_ISUID, S_ISVTX, S_IWGRP, S_IWOTH, S_IWUSR, S_IXGRP, + S_IXOTH, S_IXUSR, + }; let mut result; if display_file_type { result = String::with_capacity(10); @@ -511,31 +575,31 @@ pub fn display_permissions_unix(mode: mode_t, display_file_type: bool) -> String result = String::with_capacity(9); } - result.push(if has!(mode, S_IRUSR) { 'r' } else { '-' }); - result.push(if has!(mode, S_IWUSR) { 'w' } else { '-' }); - result.push(if has!(mode, S_ISUID as mode_t) { - if has!(mode, S_IXUSR) { 's' } else { 'S' } - } else if has!(mode, S_IXUSR) { + result.push(if mode & S_IRUSR != 0 { 'r' } else { '-' }); + result.push(if mode & S_IWUSR != 0 { 'w' } else { '-' }); + result.push(if mode & S_ISUID != 0 { + if mode & S_IXUSR != 0 { 's' } else { 'S' } + } else if mode & S_IXUSR != 0 { 'x' } else { '-' }); - result.push(if has!(mode, S_IRGRP) { 'r' } else { '-' }); - result.push(if has!(mode, S_IWGRP) { 'w' } else { '-' }); - result.push(if has!(mode, S_ISGID as mode_t) { - if has!(mode, S_IXGRP) { 's' } else { 'S' } - } else if has!(mode, S_IXGRP) { + result.push(if mode & S_IRGRP != 0 { 'r' } else { '-' }); + result.push(if mode & S_IWGRP != 0 { 'w' } else { '-' }); + result.push(if mode & S_ISGID != 0 { + if mode & S_IXGRP != 0 { 's' } else { 'S' } + } else if mode & S_IXGRP != 0 { 'x' } else { '-' }); - result.push(if has!(mode, S_IROTH) { 'r' } else { '-' }); - result.push(if has!(mode, S_IWOTH) { 'w' } else { '-' }); - result.push(if has!(mode, S_ISVTX as mode_t) { - if has!(mode, S_IXOTH) { 't' } else { 'T' } - } else if has!(mode, S_IXOTH) { + result.push(if mode & S_IROTH != 0 { 'r' } else { '-' }); + result.push(if mode & S_IWOTH != 0 { 'w' } else { '-' }); + result.push(if mode & S_ISVTX != 0 { + if mode & S_IXOTH != 0 { 't' } else { 'T' } + } else if mode & S_IXOTH != 0 { 'x' } else { '-' @@ -700,9 +764,12 @@ pub fn are_hardlinks_or_one_way_symlink_to_same_file(source: &Path, target: &Pat /// # Arguments /// /// * `path` - A reference to the path to be checked. -#[cfg(unix)] +#[cfg(any(unix, target_os = "wasi"))] pub fn path_ends_with_terminator(path: &Path) -> bool { + #[cfg(unix)] use std::os::unix::prelude::OsStrExt; + #[cfg(target_os = "wasi")] + use std::os::wasi::ffi::OsStrExt; path.as_os_str() .as_bytes() .last() @@ -730,8 +797,9 @@ pub fn path_ends_with_terminator(path: &Path) -> bool { pub fn is_stdin_directory(stdin: &Stdin) -> bool { #[cfg(unix)] { + use mode::{S_IFDIR, S_IFMT}; use nix::sys::stat::fstat; - let mode = fstat(stdin.as_fd()).unwrap().st_mode as mode_t; + let mode = fstat(stdin.as_fd()).unwrap().st_mode as u32; // We use the S_IFMT mask ala S_ISDIR() to avoid mistaking // sockets for directories. mode & S_IFMT == S_IFDIR @@ -746,11 +814,18 @@ pub fn is_stdin_directory(stdin: &Stdin) -> bool { } false } + + // WASI: stdin is never a directory + #[cfg(target_os = "wasi")] + { + let _ = stdin; + false + } } pub mod sane_blksize { - #[cfg(not(target_os = "windows"))] + #[cfg(unix)] use std::os::unix::fs::MetadataExt; use std::{fs::metadata, path::Path}; @@ -777,12 +852,12 @@ pub mod sane_blksize { #[cfg(unix)] metadata: &std::fs::Metadata, #[cfg(not(unix))] _: &std::fs::Metadata, ) -> u64 { - #[cfg(not(target_os = "windows"))] + #[cfg(unix)] { sane_blksize(metadata.blksize()) } - #[cfg(target_os = "windows")] + #[cfg(not(unix))] { DEFAULT } @@ -964,9 +1039,9 @@ mod tests { } } - #[cfg(unix)] #[test] fn test_display_permissions() { + use mode::*; // spell-checker:ignore (perms) brwsr drwxr rwxr assert_eq!( "drwxr-xr-x", @@ -992,29 +1067,29 @@ mod tests { assert_eq!( "brwSr-xr-x", - display_permissions_unix(S_IFBLK | S_ISUID as mode_t | 0o655, true) + display_permissions_unix(S_IFBLK | S_ISUID | 0o655, true) ); assert_eq!( "brwsr-xr-x", - display_permissions_unix(S_IFBLK | S_ISUID as mode_t | 0o755, true) + display_permissions_unix(S_IFBLK | S_ISUID | 0o755, true) ); assert_eq!( "prw---sr--", - display_permissions_unix(S_IFIFO | S_ISGID as mode_t | 0o614, true) + display_permissions_unix(S_IFIFO | S_ISGID | 0o614, true) ); assert_eq!( "prw---Sr--", - display_permissions_unix(S_IFIFO | S_ISGID as mode_t | 0o604, true) + display_permissions_unix(S_IFIFO | S_ISGID | 0o604, true) ); assert_eq!( "c---r-xr-t", - display_permissions_unix(S_IFCHR | S_ISVTX as mode_t | 0o055, true) + display_permissions_unix(S_IFCHR | S_ISVTX | 0o055, true) ); assert_eq!( "c---r-xr-T", - display_permissions_unix(S_IFCHR | S_ISVTX as mode_t | 0o054, true) + display_permissions_unix(S_IFCHR | S_ISVTX | 0o054, true) ); } @@ -1095,9 +1170,9 @@ mod tests { assert!(are_hardlinks_to_same_file(path1, &path2)); } - #[cfg(unix)] #[test] fn test_get_file_display() { + use mode::*; assert_eq!(get_file_display(S_IFDIR | 0o755), 'd'); assert_eq!(get_file_display(S_IFCHR | 0o644), 'c'); assert_eq!(get_file_display(S_IFBLK | 0o600), 'b'); diff --git a/src/uucore/src/lib/features/fsext.rs b/src/uucore/src/lib/features/fsext.rs index 5eacc7a4c49..0ede0f2875f 100644 --- a/src/uucore/src/lib/features/fsext.rs +++ b/src/uucore/src/lib/features/fsext.rs @@ -28,6 +28,7 @@ use crate::os_str_from_bytes; #[cfg(windows)] use crate::show_warning; +#[cfg(not(target_os = "wasi"))] use std::ffi::OsStr; #[cfg(unix)] use std::os::unix::ffi::OsStrExt; @@ -64,13 +65,14 @@ use libc::{ }; #[cfg(unix)] use std::ffi::{CStr, CString}; +#[cfg(not(target_os = "wasi"))] use std::io::Error as IOError; #[cfg(unix)] use std::mem; #[cfg(windows)] use std::path::Path; use std::time::SystemTime; -#[cfg(not(windows))] +#[cfg(unix)] use std::time::UNIX_EPOCH; use std::{borrow::Cow, ffi::OsString}; @@ -426,6 +428,7 @@ fn mount_dev_id(mount_dir: &OsStr) -> String { } } +#[cfg(not(target_os = "wasi"))] use crate::error::UResult; #[cfg(any( target_os = "freebsd", @@ -456,6 +459,7 @@ use std::ptr; use std::slice; /// Read file system list. +#[cfg(not(target_os = "wasi"))] pub fn read_fs_list() -> UResult> { #[cfg(any(target_os = "linux", target_os = "android", target_os = "cygwin"))] { @@ -535,14 +539,21 @@ pub fn read_fs_list() -> UResult> { target_os = "aix", target_os = "redox", target_os = "illumos", - target_os = "solaris" + target_os = "solaris", ))] { - // No method to read mounts, yet + // No method to read mounts on these platforms Ok(Vec::new()) } } +/// Read file system list. +#[cfg(target_os = "wasi")] +pub fn read_fs_list() -> Vec { + // No method to read mounts on WASI + Vec::new() +} + #[derive(Debug, Clone)] pub struct FsUsage { pub blocksize: u64, diff --git a/src/uucore/src/lib/features/i18n/collator.rs b/src/uucore/src/lib/features/i18n/collator.rs index 007e17c901b..9a8ee1fc649 100644 --- a/src/uucore/src/lib/features/i18n/collator.rs +++ b/src/uucore/src/lib/features/i18n/collator.rs @@ -74,6 +74,17 @@ pub fn init_locale_collation() -> bool { try_init_collator(opts) } +/// Compute the ICU collation sort key for the given input bytes and append it to `buf`. +/// This allows pre-computing sort keys once per line, then comparing them with simple +/// byte comparison during sorting (much faster than calling `compare_utf8` per comparison). +pub fn compute_sort_key_utf8(input: &[u8], buf: &mut Vec) { + let c = COLLATOR + .get() + .expect("compute_sort_key_utf8 called before collator initialization"); + c.write_sort_key_utf8_to(input, buf) + .expect("ICU write_sort_key_utf8_to failed"); +} + /// Compare both strings with regard to the current locale. pub fn locale_cmp(left: &[u8], right: &[u8]) -> Ordering { // If the detected locale is 'C', just do byte-wise comparison diff --git a/src/uucore/src/lib/features/i18n/datetime.rs b/src/uucore/src/lib/features/i18n/datetime.rs index 88816d9daed..ce52013605f 100644 --- a/src/uucore/src/lib/features/i18n/datetime.rs +++ b/src/uucore/src/lib/features/i18n/datetime.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 fieldsets prefs febr +// spell-checker:ignore fieldsets prefs febr abmon langinfo uppercased //! Locale-aware datetime formatting utilities using ICU and jiff-icu @@ -82,15 +82,27 @@ pub fn localize_format_string(format: &str, date: JiffDate) -> String { let (cal_year, cal_month, cal_day) = match calendar_type { CalendarType::Buddhist => { let d = iso_date.to_calendar(Buddhist); - (d.extended_year(), d.month().ordinal, d.day_of_month().0) + ( + d.year().extended_year(), + d.month().ordinal, + d.day_of_month().0, + ) } CalendarType::Persian => { let d = iso_date.to_calendar(Persian); - (d.extended_year(), d.month().ordinal, d.day_of_month().0) + ( + d.year().extended_year(), + d.month().ordinal, + d.day_of_month().0, + ) } CalendarType::Ethiopian => { let d = iso_date.to_calendar(Ethiopian::new()); - (d.extended_year(), d.month().ordinal, d.day_of_month().0) + ( + d.year().extended_year(), + d.month().ordinal, + d.day_of_month().0, + ) } CalendarType::Gregorian => unreachable!(), }; @@ -137,10 +149,165 @@ pub fn localize_format_string(format: &str, date: JiffDate) -> String { fmt.replace(PERCENT_PLACEHOLDER, "%%") } +/// Abbreviated month names for the current LC_TIME locale as raw bytes, +/// with blanks stripped and uppercased using ASCII case folding. +/// +/// Each entry corresponds to months January (index 0) through December (index 11). +/// Returns `None` for C/POSIX locale (caller should use English defaults). +/// This matches the GNU coreutils approach of storing uppercased, blank-stripped names. +pub fn get_locale_months() -> Option<&'static [Vec; 12]> { + static LOCALE_MONTHS: OnceLock; 12]>> = OnceLock::new(); + + LOCALE_MONTHS + .get_or_init(|| { + if !should_use_icu_locale() { + return None; + } + get_locale_months_inner() + }) + .as_ref() +} + +/// Unix implementation using nl_langinfo for exact match with `locale abmon` output. +#[cfg(any( + target_os = "linux", + target_vendor = "apple", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd", + target_os = "dragonfly" +))] +fn get_locale_months_inner() -> Option<[Vec; 12]> { + use nix::libc; + use std::ffi::CStr; + + let abmon_items: [libc::nl_item; 12] = [ + libc::ABMON_1, + libc::ABMON_2, + libc::ABMON_3, + libc::ABMON_4, + libc::ABMON_5, + libc::ABMON_6, + libc::ABMON_7, + libc::ABMON_8, + libc::ABMON_9, + libc::ABMON_10, + libc::ABMON_11, + libc::ABMON_12, + ]; + + // SAFETY: setlocale and nl_langinfo are standard POSIX functions. + // We call setlocale(LC_TIME, "") to initialize from environment variables, + // then read the abbreviated month names. This is called once (via OnceLock) + // and cached, so the race window with other setlocale callers is minimal. + // The nl_langinfo return pointer is immediately copied below. + unsafe { + libc::setlocale(libc::LC_TIME, c"".as_ptr()); + } + + let mut months: [Vec; 12] = Default::default(); + for (i, &item) in abmon_items.iter().enumerate() { + // SAFETY: nl_langinfo returns a valid C string pointer for valid nl_item values. + let ptr = unsafe { libc::nl_langinfo(item) }; + if ptr.is_null() { + return None; + } + let name = unsafe { CStr::from_ptr(ptr) }.to_bytes(); + if name.is_empty() { + return None; + } + // Strip blanks and uppercase using ASCII case folding, matching GNU behavior + months[i] = name + .iter() + .filter(|&&b| !b.is_ascii_whitespace()) + .map(|&b| b.to_ascii_uppercase()) + .collect(); + } + + Some(months) +} + +/// Non-Unix fallback using ICU DateTimeFormatter. +#[cfg(not(any( + target_os = "linux", + target_vendor = "apple", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd", + target_os = "dragonfly" +)))] +fn get_locale_months_inner() -> Option<[Vec; 12]> { + let (locale, _) = get_time_locale(); + let locale_prefs = locale.clone().into(); + // M::medium() produces abbreviated month names (e.g. "Jan", "Feb") matching + // nl_langinfo(ABMON_*) on Unix. M::short() produces numeric ("1", "2") and + // M::long() produces full names ("January", "February"). + let formatter = DateTimeFormatter::try_new(locale_prefs, fieldsets::M::medium()).ok()?; + + let mut months: [Vec; 12] = Default::default(); + for i in 0..12u8 { + let iso_date = Date::::try_new_iso(2000, i + 1, 1).ok()?; + let formatted = formatter.format(&iso_date).to_string(); + // Strip blanks and uppercase using ASCII case folding + months[i as usize] = formatted + .bytes() + .filter(|b| !b.is_ascii_whitespace()) + .map(|b| b.to_ascii_uppercase()) + .collect(); + } + + Some(months) +} + #[cfg(test)] mod tests { use super::*; + /// Verify that ICU `M::medium()` produces abbreviated month names matching + /// what `nl_langinfo(ABMON_*)` returns on Unix. This is the format used by + /// the non-Unix fallback in `get_locale_months_inner`. + #[test] + fn test_icu_medium_month_produces_abbreviated_names() { + use icu_locale::locale; + + let locale: Locale = locale!("en-US"); + let formatter = DateTimeFormatter::try_new(locale.into(), fieldsets::M::medium()).unwrap(); + + let expected = [ + "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", + ]; + + for (i, exp) in expected.iter().enumerate() { + let iso_date = Date::::try_new_iso(2000, (i + 1) as u8, 1).unwrap(); + let formatted = formatter.format(&iso_date).to_string(); + assert_eq!( + &formatted, + exp, + "M::medium() for month {} should produce abbreviated name", + i + 1 + ); + } + } + + /// Confirm that M::short() gives numeric months and M::long() gives full names, + /// so M::medium() is the only correct choice for abbreviated month names. + #[test] + fn test_icu_short_and_long_month_formats_differ() { + use icu_locale::locale; + + let locale: Locale = locale!("en-US"); + let iso_jan = Date::::try_new_iso(2000, 1, 1).unwrap(); + + let short_fmt = + DateTimeFormatter::try_new(locale.clone().into(), fieldsets::M::short()).unwrap(); + let long_fmt = DateTimeFormatter::try_new(locale.into(), fieldsets::M::long()).unwrap(); + + // M::short() produces numeric ("1"), not "Jan" + assert_eq!(short_fmt.format(&iso_jan).to_string(), "1"); + // M::long() produces full name ("January"), not "Jan" + assert_eq!(long_fmt.format(&iso_jan).to_string(), "January"); + } + #[test] fn test_calendar_type_detection() { use icu_locale::locale; diff --git a/src/uucore/src/lib/features/mode.rs b/src/uucore/src/lib/features/mode.rs index 4e737595ee2..6a96307b2b7 100644 --- a/src/uucore/src/lib/features/mode.rs +++ b/src/uucore/src/lib/features/mode.rs @@ -7,7 +7,7 @@ // spell-checker:ignore (vars) fperm srwx -#[cfg(not(unix))] +#[cfg(windows)] use libc::umask; pub fn parse_numeric(fperm: u32, mut mode: &str, considering_dir: bool) -> Result { @@ -185,7 +185,7 @@ pub fn get_umask() -> u32 { mask.bits() as u32 } - #[cfg(not(unix))] + #[cfg(windows)] { // SAFETY: umask always succeeds and doesn't operate on memory. Races are // possible but it can't violate Rust's guarantees. @@ -193,6 +193,12 @@ pub fn get_umask() -> u32 { unsafe { umask(mask) }; mask as u32 } + + // WASI has no umask; return a typical default (022). + #[cfg(not(any(unix, windows)))] + { + 0o022 + } } #[cfg(test)] diff --git a/src/uucore/src/lib/features/parser/parse_size.rs b/src/uucore/src/lib/features/parser/parse_size.rs index cc7e5e9e2e4..15a6d58b8c5 100644 --- a/src/uucore/src/lib/features/parser/parse_size.rs +++ b/src/uucore/src/lib/features/parser/parse_size.rs @@ -6,6 +6,42 @@ //! Parser for sizes in SI or IEC units (multiples of 1000 or 1024 bytes). +/// The first eleven powers of 1000: `1, 10^3, 10^6, ..., 10^30`. +/// +/// Index `n` is the SI base for the `n`-th suffix (0 → no suffix, 1 → K/kB, +/// 2 → M/MB, ...). +pub const SI_BASES: [u128; 11] = [ + 1, + 1_000, + 1_000_000, + 1_000_000_000, + 1_000_000_000_000, + 1_000_000_000_000_000, + 1_000_000_000_000_000_000, + 1_000_000_000_000_000_000_000, + 1_000_000_000_000_000_000_000_000, + 1_000_000_000_000_000_000_000_000_000, + 1_000_000_000_000_000_000_000_000_000_000, +]; + +/// The first eleven powers of 1024: `1, 1024, 1024^2, ..., 1024^10`. +/// +/// Index `n` is the IEC base for the `n`-th suffix (0 → no suffix, 1 → Ki, +/// 2 → Mi, ...). +pub const IEC_BASES: [u128; 11] = [ + 1, + 1_024, + 1_048_576, + 1_073_741_824, + 1_099_511_627_776, + 1_125_899_906_842_624, + 1_152_921_504_606_846_976, + 1_180_591_620_717_411_303_424, + 1_208_925_819_614_629_174_706_176, + 1_237_940_039_285_380_274_899_124_224, + 1_267_650_600_228_229_401_496_703_205_376, +]; + use std::error::Error; use std::fmt; use std::num::{IntErrorKind, ParseIntError}; diff --git a/src/uucore/src/lib/features/pipes.rs b/src/uucore/src/lib/features/pipes.rs index 5ac590b7b4a..cb96908aad9 100644 --- a/src/uucore/src/lib/features/pipes.rs +++ b/src/uucore/src/lib/features/pipes.rs @@ -3,29 +3,34 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -//! Thin pipe-related wrappers around functions from the `nix` crate. +//! Thin zero-copy-related wrappers around functions from the `rustix` crate. -use std::fs::File; #[cfg(any(target_os = "linux", target_os = "android"))] -use std::io::IoSlice; +use rustix::pipe::{SpliceFlags, fcntl_setpipe_size}; +#[cfg(any(target_os = "linux", target_os = "android"))] +use std::fs::File; #[cfg(any(target_os = "linux", target_os = "android"))] use std::os::fd::AsFd; - #[cfg(any(target_os = "linux", target_os = "android"))] -use nix::fcntl::SpliceFFlags; +pub const MAX_ROOTLESS_PIPE_SIZE: usize = 1024 * 1024; -pub use nix::{Error, Result}; - -/// A wrapper around [`nix::unistd::pipe`] that ensures the pipe is cleaned up. +/// A wrapper around [`rustix::pipe::pipe`] that ensures the pipe is cleaned up. /// /// Returns two `File` objects: everything written to the second can be read /// from the first. -pub fn pipe() -> Result<(File, File)> { - let (read, write) = nix::unistd::pipe()?; +/// used for resolving the limitation for splice: one of a input or output should be pipe +#[inline] +#[cfg(any(target_os = "linux", target_os = "android", test))] +pub fn pipe() -> std::io::Result<(File, File)> { + let (read, write) = rustix::pipe::pipe()?; + // improve performance for splice + #[cfg(any(target_os = "linux", target_os = "android"))] + let _ = fcntl_setpipe_size(&read, MAX_ROOTLESS_PIPE_SIZE); + Ok((File::from(read), File::from(write))) } -/// Less noisy wrapper around [`nix::fcntl::splice`]. +/// Less noisy wrapper around [`rustix::pipe::splice`]. /// /// Up to `len` bytes are moved from `source` to `target`. Returns the number /// of successfully moved bytes. @@ -34,9 +39,17 @@ pub fn pipe() -> Result<(File, File)> { /// To get around this requirement, consider splicing from your source into /// a [`pipe`] and then from the pipe into your target (with `splice_exact`): /// this is still very efficient. +#[inline] #[cfg(any(target_os = "linux", target_os = "android"))] -pub fn splice(source: &impl AsFd, target: &impl AsFd, len: usize) -> Result { - nix::fcntl::splice(source, None, target, None, len, SpliceFFlags::empty()) +pub fn splice(source: &impl AsFd, target: &impl AsFd, len: usize) -> std::io::Result { + Ok(rustix::pipe::splice( + source, + None, + target, + None, + len, + SpliceFlags::empty(), + )?) } /// Splice wrapper which fully finishes the write. @@ -44,21 +57,33 @@ pub fn splice(source: &impl AsFd, target: &impl AsFd, len: usize) -> Result Result<()> { +pub fn splice_exact(source: &impl AsFd, target: &impl AsFd, len: usize) -> std::io::Result<()> { let mut left = len; - while left != 0 { + while left > 0 { let written = splice(source, target, left)?; - assert_ne!(written, 0, "unexpected end of data"); + debug_assert_ne!(written, 0, "unexpected end of data"); left -= written; } Ok(()) } -/// Copy data from `bytes` into `target`, which must be a pipe. +/// Return verified /dev/null /// -/// Returns the number of successfully copied bytes. +/// `splice` to /dev/null is faster than `read` when we skip or count the input which is not able to seek +#[inline] #[cfg(any(target_os = "linux", target_os = "android"))] -pub fn vmsplice(target: &impl AsFd, bytes: &[u8]) -> Result { - nix::fcntl::vmsplice(target, &[IoSlice::new(bytes)], SpliceFFlags::empty()) +pub fn dev_null() -> Option { + let null = std::fs::OpenOptions::new() + .write(true) + .open("/dev/null") + .ok()?; + let stat = rustix::fs::fstat(&null).ok()?; + let dev = stat.st_rdev; + if (rustix::fs::major(dev), rustix::fs::minor(dev)) == (1, 3) { + Some(null) + } else { + None + } } diff --git a/src/uucore/src/lib/features/signals.rs b/src/uucore/src/lib/features/signals.rs index 6b89704fcad..275f7acd266 100644 --- a/src/uucore/src/lib/features/signals.rs +++ b/src/uucore/src/lib/features/signals.rs @@ -396,11 +396,24 @@ pub static ALL_SIGNALS: [&str; 32] = [ pub fn signal_by_name_or_value(signal_name_or_value: &str) -> Option { let signal_name_upcase = signal_name_or_value.to_uppercase(); if let Ok(value) = signal_name_upcase.parse() { - return if is_signal(value) { Some(value) } else { None }; + if is_signal(value) { + return Some(value); + } + return realtime_signal_bounds() + .filter(|&(rtmin, rtmax)| value >= rtmin && value <= rtmax) + .map(|_| value); } let signal_name = signal_name_upcase.trim_start_matches("SIG"); - ALL_SIGNALS.iter().position(|&s| s == signal_name) + if let Some(pos) = ALL_SIGNALS.iter().position(|&s| s == signal_name) { + return Some(pos); + } + + realtime_signal_bounds().and_then(|(rtmin, rtmax)| match signal_name { + "RTMIN" => Some(rtmin), + "RTMAX" => Some(rtmax), + _ => None, + }) } /// Returns true if the given number is a valid signal number. @@ -525,6 +538,9 @@ static STDERR_WAS_CLOSED: AtomicBool = AtomicBool::new(false); #[cfg(unix)] static SIGPIPE_WAS_IGNORED: AtomicBool = AtomicBool::new(false); +#[cfg(unix)] +static STARTUP_STATE_WAS_CAPTURED: AtomicBool = AtomicBool::new(false); + /// Captures stdio and SIGPIPE state at process initialization, before main() runs. /// /// # Safety @@ -536,6 +552,11 @@ pub unsafe extern "C" fn capture_startup_state() { use std::mem::MaybeUninit; use std::ptr; + // No spinlock because we're single-threaded at this point + if STARTUP_STATE_WAS_CAPTURED.swap(true, Ordering::Relaxed) { + return; + } + // Capture stdio state unsafe { STDIN_WAS_CLOSED.store( @@ -754,3 +775,19 @@ fn linux_unnamed_signal_numbers_are_valid_for_lists() { assert_eq!(signal_list_value_by_name_or_number("32"), Some(32)); assert_eq!(signal_list_value_by_name_or_number("33"), Some(33)); } + +#[cfg(any(target_os = "linux", target_os = "android"))] +#[test] +fn linux_realtime_signals_resolve_by_name_or_value() { + let (rtmin, rtmax) = realtime_signal_bounds().unwrap(); + + // By name + assert_eq!(signal_by_name_or_value("RTMIN"), Some(rtmin)); + assert_eq!(signal_by_name_or_value("RTMAX"), Some(rtmax)); + assert_eq!(signal_by_name_or_value("SIGRTMIN"), Some(rtmin)); + assert_eq!(signal_by_name_or_value("SIGRTMAX"), Some(rtmax)); + + // By numeric value + assert_eq!(signal_by_name_or_value(&rtmin.to_string()), Some(rtmin)); + assert_eq!(signal_by_name_or_value(&rtmax.to_string()), Some(rtmax)); +} diff --git a/src/uucore/src/lib/features/sum.rs b/src/uucore/src/lib/features/sum.rs index 279643a962b..7a9f1a5ce58 100644 --- a/src/uucore/src/lib/features/sum.rs +++ b/src/uucore/src/lib/features/sum.rs @@ -83,9 +83,15 @@ pub struct Blake2b { impl Blake2b { pub const DEFAULT_BYTE_SIZE: usize = 64; + pub const DEFAULT_BIT_SIZE: usize = Self::DEFAULT_BYTE_SIZE * 8; /// Return a new Blake2b instance with a custom output bytes length pub fn with_output_bytes(output_bytes: usize) -> Self { + debug_assert!( + output_bytes <= Self::DEFAULT_BYTE_SIZE, + "GNU doesn't accept BLAKE2b bigger than 64 bytes long" + ); + let mut params = blake2b_simd::Params::new(); params.hash_length(output_bytes); @@ -122,31 +128,58 @@ impl Digest for Blake2b { } } -#[derive(Default)] -pub struct Blake3(blake3::Hasher); +pub struct Blake3 { + digest: blake3::Hasher, + byte_size: usize, +} + +impl Blake3 { + /// Default length for the BLAKE3 digest in bytes. + pub const DEFAULT_BYTE_SIZE: usize = 32; + + pub fn with_output_bytes(output_bytes: usize) -> Self { + Self { + digest: blake3::Hasher::new(), + byte_size: output_bytes, + } + } +} + +impl Default for Blake3 { + fn default() -> Self { + Self { + digest: blake3::Hasher::default(), + byte_size: Self::DEFAULT_BYTE_SIZE, + } + } +} impl Digest for Blake3 { fn hash_update(&mut self, input: &[u8]) { - self.0.update(input); + self.digest.update(input); } fn hash_finalize(&mut self, out: &mut [u8]) { - let hash_result = &self.0.finalize(); - out.copy_from_slice(hash_result.as_bytes()); + let mut hash_result = self.digest.finalize_xof(); + hash_result.fill(out); } fn reset(&mut self) { - *self = Self::default(); + *self = Self::with_output_bytes(self.output_bytes()); } fn output_bits(&self) -> usize { - 256 + self.byte_size * 8 } } #[derive(Default)] pub struct Sm3(sm3::Sm3); +impl Sm3 { + pub const BIT_SIZE: usize = 256; +} + impl Digest for Sm3 { fn hash_update(&mut self, input: &[u8]) { ::update(&mut self.0, input); @@ -161,7 +194,7 @@ impl Digest for Sm3 { } fn output_bits(&self) -> usize { - 256 + Self::BIT_SIZE } } @@ -338,6 +371,9 @@ impl Digest for SysV { // Implements the Digest trait for sha2 / sha3 algorithms with fixed output macro_rules! impl_digest_common { ($algo_type: ty, $size: literal) => { + impl $algo_type { + pub const BIT_SIZE: usize = $size; + } impl Default for $algo_type { fn default() -> Self { Self(Default::default()) @@ -357,7 +393,7 @@ macro_rules! impl_digest_common { } fn output_bits(&self) -> usize { - $size + Self::BIT_SIZE } } }; diff --git a/src/uucore/src/lib/lib.rs b/src/uucore/src/lib/lib.rs index 4b2cb9b502b..27548688987 100644 --- a/src/uucore/src/lib/lib.rs +++ b/src/uucore/src/lib/lib.rs @@ -142,6 +142,8 @@ use std::io::{BufRead, BufReader}; use std::iter; #[cfg(unix)] use std::os::unix::ffi::{OsStrExt, OsStringExt}; +#[cfg(target_os = "wasi")] +use std::os::wasi::ffi::{OsStrExt, OsStringExt}; use std::str; use std::str::Utf8Chunk; use std::sync::{LazyLock, atomic::Ordering}; @@ -434,12 +436,12 @@ impl error::UError for NonUtf8OsStrError {} /// /// This always succeeds on unix platforms, /// and fails on other platforms if the string can't be coerced to UTF-8. -#[cfg_attr(unix, expect(clippy::unnecessary_wraps))] +#[cfg_attr(any(unix, target_os = "wasi"), expect(clippy::unnecessary_wraps))] pub fn os_str_as_bytes(os_string: &OsStr) -> Result<&[u8], NonUtf8OsStrError> { - #[cfg(unix)] + #[cfg(any(unix, target_os = "wasi"))] return Ok(os_string.as_bytes()); - #[cfg(not(unix))] + #[cfg(not(any(unix, target_os = "wasi")))] os_string .to_str() .ok_or_else(|| NonUtf8OsStrError { @@ -453,10 +455,10 @@ pub fn os_str_as_bytes(os_string: &OsStr) -> Result<&[u8], NonUtf8OsStrError> { /// 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)] + #[cfg(any(unix, target_os = "wasi"))] return Cow::from(os_string.as_bytes()); - #[cfg(not(unix))] + #[cfg(not(any(unix, target_os = "wasi")))] match os_string.to_string_lossy() { Cow::Borrowed(slice) => Cow::from(slice.as_bytes()), Cow::Owned(owned) => Cow::from(owned.into_bytes()), @@ -468,12 +470,12 @@ pub fn os_str_as_bytes_lossy(os_string: &OsStr) -> Cow<'_, [u8]> { /// /// This always succeeds on unix platforms, /// and fails on other platforms if the bytes can't be parsed as UTF-8. -#[cfg_attr(unix, expect(clippy::unnecessary_wraps))] +#[cfg_attr(any(unix, target_os = "wasi"), expect(clippy::unnecessary_wraps))] pub fn os_str_from_bytes(bytes: &[u8]) -> error::UResult> { - #[cfg(unix)] + #[cfg(any(unix, target_os = "wasi"))] return Ok(Cow::Borrowed(OsStr::from_bytes(bytes))); - #[cfg(not(unix))] + #[cfg(not(any(unix, target_os = "wasi")))] Ok(Cow::Owned(OsString::from(str::from_utf8(bytes).map_err( |_| error::UUsageError::new(1, "Unable to transform bytes into OsStr"), )?))) @@ -483,12 +485,12 @@ pub fn os_str_from_bytes(bytes: &[u8]) -> error::UResult> { /// /// This always succeeds on unix platforms, /// and fails on other platforms if the bytes can't be parsed as UTF-8. -#[cfg_attr(unix, expect(clippy::unnecessary_wraps))] +#[cfg_attr(any(unix, target_os = "wasi"), expect(clippy::unnecessary_wraps))] pub fn os_string_from_vec(vec: Vec) -> error::UResult { - #[cfg(unix)] + #[cfg(any(unix, target_os = "wasi"))] return Ok(OsString::from_vec(vec)); - #[cfg(not(unix))] + #[cfg(not(any(unix, target_os = "wasi")))] Ok(OsString::from(String::from_utf8(vec).map_err(|_| { error::UUsageError::new(1, "invalid UTF-8 was detected in one or more arguments") })?)) @@ -498,11 +500,11 @@ pub fn os_string_from_vec(vec: Vec) -> error::UResult { /// /// This always succeeds on unix platforms, /// and fails on other platforms if the bytes can't be parsed as UTF-8. -#[cfg_attr(unix, expect(clippy::unnecessary_wraps))] +#[cfg_attr(any(unix, target_os = "wasi"), expect(clippy::unnecessary_wraps))] pub fn os_string_to_vec(s: OsString) -> error::UResult> { - #[cfg(unix)] + #[cfg(any(unix, target_os = "wasi"))] let v = s.into_vec(); - #[cfg(not(unix))] + #[cfg(not(any(unix, target_os = "wasi")))] let v = s .into_string() .map_err(|_| { diff --git a/src/uucore/src/lib/mods/io.rs b/src/uucore/src/lib/mods/io.rs index c83c3d98744..40bd74324fb 100644 --- a/src/uucore/src/lib/mods/io.rs +++ b/src/uucore/src/lib/mods/io.rs @@ -73,8 +73,22 @@ impl OwnedFileDescriptorOrHandle { } /// instantiates a corresponding `Stdio` + #[cfg(not(target_os = "wasi"))] pub fn into_stdio(self) -> Stdio { - Stdio::from(self.fx) + #[cfg(not(target_os = "wasi"))] + { + Stdio::from(self.fx) + } + #[cfg(target_os = "wasi")] + { + Stdio::from(File::from(self.fx)) + } + } + + /// WASI: Stdio::from(OwnedFd) is not available, convert via File instead. + #[cfg(target_os = "wasi")] + pub fn into_stdio(self) -> Stdio { + Stdio::from(File::from(self.fx)) } /// clones self. useful when needing another diff --git a/src/uucore/src/lib/mods/locale.rs b/src/uucore/src/lib/mods/locale.rs index c6117c80bff..f25866bc144 100644 --- a/src/uucore/src/lib/mods/locale.rs +++ b/src/uucore/src/lib/mods/locale.rs @@ -308,6 +308,41 @@ fn create_english_bundle_from_embedded( } } +/// Create a bundle from embedded locale files for any locale on WASI. +/// Bypasses the global OnceLock cache (uses Box::leak) so it can be +/// called for multiple locales in the same process. +#[cfg(target_os = "wasi")] +fn create_wasi_bundle_from_embedded( + locale: &LanguageIdentifier, + util_name: &str, +) -> Result, LocalizationError> { + let locale_str = locale.to_string(); + let mut bundle: FluentBundle<&'static FluentResource> = FluentBundle::new(vec![locale.clone()]); + bundle.set_use_isolating(false); + + let mut try_add = |key: &str| { + if let Some(content) = get_embedded_locale(key) { + if let Ok(resource) = FluentResource::try_new(content.to_string()) { + bundle.add_resource_overriding(Box::leak(Box::new(resource))); + } + } + }; + + try_add(&format!("uucore/{locale_str}.ftl")); + if util_name.ends_with("sum") { + try_add(&format!("checksum_common/{locale_str}.ftl")); + } + try_add(&format!("{util_name}/{locale_str}.ftl")); + + if bundle.has_message("common-error") || bundle.has_message(&format!("{util_name}-about")) { + Ok(bundle) + } else { + Err(LocalizationError::LocalesDirNotFound(format!( + "No embedded locale found for {util_name}/{locale_str}" + ))) + } +} + fn get_message_internal(id: &str, args: Option) -> String { LOCALIZER.with(|lock| { lock.get() @@ -446,12 +481,27 @@ pub fn setup_localization(p: &str) -> Result<(), LocalizationError> { // Load both utility-specific and common strings init_localization(&locale, &locales_dir, p)?; } else { - // No locales directory found, use embedded English with common strings directly + // No locales directory found, use embedded locales let default_locale = LanguageIdentifier::from_str(DEFAULT_LOCALE) .expect("Default locale should always be valid"); - let english_bundle: FluentBundle<&'static FluentResource> = - create_english_bundle_from_embedded(&default_locale, p)?; - let localizer = Localizer::new(english_bundle); + + #[cfg(target_os = "wasi")] + let localizer = { + let english_bundle = create_wasi_bundle_from_embedded(&default_locale, p)?; + if locale == default_locale { + Localizer::new(english_bundle) + } else if let Ok(localized) = create_wasi_bundle_from_embedded(&locale, p) { + Localizer::new(localized).with_fallback(english_bundle) + } else { + Localizer::new(english_bundle) + } + }; + + #[cfg(not(target_os = "wasi"))] + let localizer = { + let english_bundle = create_english_bundle_from_embedded(&default_locale, p)?; + Localizer::new(english_bundle) + }; LOCALIZER.with(|lock| { lock.set(localizer) diff --git a/tests/by-util/test_chown.rs b/tests/by-util/test_chown.rs index 2602aabde06..f7c08b4a054 100644 --- a/tests/by-util/test_chown.rs +++ b/tests/by-util/test_chown.rs @@ -149,7 +149,8 @@ fn test_chown_only_owner_colon() { .arg("--verbose") .arg(file1) .succeeds() - .stderr_contains("retained as"); + .stderr_contains("retained as") + .stderr_contains("warning: '.' should be ':'"); scene .ucmd() @@ -160,6 +161,66 @@ fn test_chown_only_owner_colon() { .stderr_contains("failed to change"); } +#[test] +fn test_chown_dot_separator_warning() { + // test that using '.' as separator emits a warning + + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let result = scene.cmd("whoami").run(); + if skipping_test_is_okay(&result, "whoami: cannot find name for user ID") { + return; + } + let user_name = String::from(result.stdout_str().trim()); + assert!(!user_name.is_empty()); + + let file1 = "test_chown_dot_warn"; + at.touch(file1); + + let result = scene.cmd("id").arg("-gn").run(); + if skipping_test_is_okay(&result, "id: cannot find name for group ID") { + return; + } + let group_name = String::from(result.stdout_str().trim()); + assert!(!group_name.is_empty()); + + // chown user. file should warn about '.' separator + scene + .ucmd() + .arg(format!("{user_name}.")) + .arg(file1) + .succeeds() + .stderr_contains("warning: '.' should be ':'"); + + // chown user.group file should warn AND apply both owner and group + let result = scene + .ucmd() + .arg(format!("{user_name}.{group_name}")) + .arg("--verbose") + .arg(file1) + .run(); + if skipping_test_is_okay(&result, "chown: invalid group:") { + return; + } + result.stderr_contains("warning: '.' should be ':'"); + // "retained as" on Linux, "changed ownership" on BSDs (group inherited from parent dir) + assert!( + result.stderr_str().contains("retained as") + || result.stderr_str().contains("changed ownership"), + "expected verbose ownership output, got: {}", + result.stderr_str() + ); + + // chown user: file should not warn + scene + .ucmd() + .arg(format!("{user_name}:")) + .arg(file1) + .succeeds() + .stderr_does_not_contain("warning"); +} + #[test] fn test_chown_only_colon() { // test chown : file.txt @@ -494,7 +555,7 @@ fn test_chown_only_group_id() { } result.stderr_contains("retained as"); - // Apparently on CI "macos-latest, x86_64-apple-darwin, feat_os_macos" + // Apparently on CI "macos-latest, x86_64-apple-darwin, feat_os_unix" // the process has the rights to change from runner:staff to runner:wheel #[cfg(any(windows, all(unix, not(target_os = "macos"))))] // FreeBSD user on CI is part of wheel group diff --git a/tests/by-util/test_cksum.rs b/tests/by-util/test_cksum.rs index 67f96e90762..16ed6d8b373 100644 --- a/tests/by-util/test_cksum.rs +++ b/tests/by-util/test_cksum.rs @@ -5,17 +5,42 @@ // spell-checker:ignore (words) asdf algo algos asha mgmt xffname hexa GFYEQ HYQK Yqxb dont checkfile use rstest::rstest; +use rstest_reuse::{apply, template}; use uutests::at_and_ucmd; use uutests::new_ucmd; use uutests::util::TestScenario; +use uutests::util::UCommand; use uutests::util::log_info; use uutests::util_name; -const ALGOS: [&str; 11] = [ - "sysv", "bsd", "crc", "md5", "sha1", "sha224", "sha256", "sha384", "sha512", "blake2b", "sm3", -]; -const SHA_LENGTHS: [u32; 4] = [224, 256, 384, 512]; +#[template] +#[rstest] +#[case::sysv("sysv")] +#[case::bsd("bsd")] +#[case::crc("crc")] +#[case::md5("md5")] +#[case::sha1("sha1")] +#[case::sha224("sha224")] +#[case::sha256("sha256")] +#[case::sha384("sha384")] +#[case::sha512("sha512")] +#[case::blake2b("blake2b")] +#[case::blake3("blake3")] +#[case::sm3("sm3")] +fn test_all_algos(#[case] algo: &str) {} + +#[template] +#[rstest] +fn test_sha(#[values("sha2", "sha3")] algo: &str, #[values(224, 256, 384, 512)] len: u32) {} + +fn sha_fixture_name(algo: &str, len: u32, prefix: &str, suffix: &str) -> String { + // assume algo is always "sha2" or "sha3" + match algo { + "sha2" => format!("{prefix}sha{len}{suffix}"), + _ => format!("{prefix}sha3_{len}{suffix}"), + } +} #[test] fn test_invalid_arg() { @@ -169,43 +194,37 @@ fn test_tag_after_untagged() { .stdout_is_fixture("md5_single_file.expected"); } -#[test] -fn test_algorithm_single_file() { - for algo in ALGOS { - for option in ["-a", "--algorithm"] { - new_ucmd!() - .arg(format!("{option}={algo}")) - .arg("lorem_ipsum.txt") - .succeeds() - .stdout_is_fixture(format!("{algo}_single_file.expected")); - } +#[apply(test_all_algos)] +fn test_algorithm_single_file(#[case] algo: &str) { + for option in ["-a", "--algorithm"] { + new_ucmd!() + .arg(format!("{option}={algo}")) + .arg("lorem_ipsum.txt") + .succeeds() + .stdout_is_fixture(format!("{algo}_single_file.expected")); } } -#[test] -fn test_algorithm_multiple_files() { - for algo in ALGOS { - for option in ["-a", "--algorithm"] { - new_ucmd!() - .arg(format!("{option}={algo}")) - .arg("lorem_ipsum.txt") - .arg("alice_in_wonderland.txt") - .succeeds() - .stdout_is_fixture(format!("{algo}_multiple_files.expected")); - } +#[apply(test_all_algos)] +fn test_algorithm_multiple_files(#[case] algo: &str) { + for option in ["-a", "--algorithm"] { + new_ucmd!() + .arg(format!("{option}={algo}")) + .arg("lorem_ipsum.txt") + .arg("alice_in_wonderland.txt") + .succeeds() + .stdout_is_fixture(format!("{algo}_multiple_files.expected")); } } -#[test] -fn test_algorithm_stdin() { - for algo in ALGOS { - for option in ["-a", "--algorithm"] { - new_ucmd!() - .arg(format!("{option}={algo}")) - .pipe_in_fixture("lorem_ipsum.txt") - .succeeds() - .stdout_is_fixture(format!("{algo}_stdin.expected")); - } +#[apply(test_all_algos)] +fn test_algorithm_stdin(#[case] algo: &str) { + for option in ["-a", "--algorithm"] { + new_ucmd!() + .arg(format!("{option}={algo}")) + .pipe_in_fixture("lorem_ipsum.txt") + .succeeds() + .stdout_is_fixture(format!("{algo}_stdin.expected")); } } @@ -237,16 +256,14 @@ fn test_untagged_stdin() { .stdout_is_fixture("untagged/crc_stdin.expected"); } -#[test] -fn test_untagged_algorithm_single_file() { - for algo in ALGOS { - new_ucmd!() - .arg("--untagged") - .arg(format!("--algorithm={algo}")) - .arg("lorem_ipsum.txt") - .succeeds() - .stdout_is_fixture(format!("untagged/{algo}_single_file.expected")); - } +#[apply(test_all_algos)] +fn test_untagged_algorithm_single_file(#[case] algo: &str) { + new_ucmd!() + .arg("--untagged") + .arg(format!("--algorithm={algo}")) + .arg("lorem_ipsum.txt") + .succeeds() + .stdout_is_fixture(format!("untagged/{algo}_single_file.expected")); } #[test] @@ -271,29 +288,25 @@ fn test_untagged_algorithm_after_tag() { .stdout_is_fixture("untagged/md5_single_file.expected"); } -#[test] -fn test_untagged_algorithm_multiple_files() { - for algo in ALGOS { - new_ucmd!() - .arg("--untagged") - .arg(format!("--algorithm={algo}")) - .arg("lorem_ipsum.txt") - .arg("alice_in_wonderland.txt") - .succeeds() - .stdout_is_fixture(format!("untagged/{algo}_multiple_files.expected")); - } +#[apply(test_all_algos)] +fn test_untagged_algorithm_multiple_files(#[case] algo: &str) { + new_ucmd!() + .arg("--untagged") + .arg(format!("--algorithm={algo}")) + .arg("lorem_ipsum.txt") + .arg("alice_in_wonderland.txt") + .succeeds() + .stdout_is_fixture(format!("untagged/{algo}_multiple_files.expected")); } -#[test] -fn test_untagged_algorithm_stdin() { - for algo in ALGOS { - new_ucmd!() - .arg("--untagged") - .arg(format!("--algorithm={algo}")) - .pipe_in_fixture("lorem_ipsum.txt") - .succeeds() - .stdout_is_fixture(format!("untagged/{algo}_stdin.expected")); - } +#[apply(test_all_algos)] +fn test_untagged_algorithm_stdin(#[case] algo: &str) { + new_ucmd!() + .arg("--untagged") + .arg(format!("--algorithm={algo}")) + .pipe_in_fixture("lorem_ipsum.txt") + .succeeds() + .stdout_is_fixture(format!("untagged/{algo}_stdin.expected")); } #[test] @@ -373,131 +386,169 @@ fn test_sha_missing_length() { } } -#[test] -fn test_sha2_single_file() { - for l in SHA_LENGTHS { - new_ucmd!() - .arg("--algorithm=sha2") - .arg(format!("--length={l}")) - .arg("lorem_ipsum.txt") - .succeeds() - .stdout_is_fixture(format!("sha{l}_single_file.expected")); - } +fn sha_cmd(algo: &str, len: u32) -> UCommand { + let mut ucmd = new_ucmd!(); + ucmd.arg(format!("--algorithm={algo}")) + .arg(format!("--length={len}")); + ucmd } -#[test] -fn test_sha2_multiple_files() { - for l in SHA_LENGTHS { - new_ucmd!() - .arg("--algorithm=sha2") - .arg(format!("--length={l}")) - .arg("lorem_ipsum.txt") - .arg("alice_in_wonderland.txt") - .succeeds() - .stdout_is_fixture(format!("sha{l}_multiple_files.expected")); - } +#[apply(test_sha)] +fn test_sha_single_file(algo: &str, len: u32) { + sha_cmd(algo, len) + .arg("lorem_ipsum.txt") + .succeeds() + .stdout_is_fixture(sha_fixture_name(algo, len, "", "_single_file.expected")); } -#[test] -fn test_sha2_stdin() { - for l in SHA_LENGTHS { - new_ucmd!() - .arg("--algorithm=sha2") - .arg(format!("--length={l}")) - .pipe_in_fixture("lorem_ipsum.txt") - .succeeds() - .stdout_is_fixture(format!("sha{l}_stdin.expected")); - } +#[apply(test_sha)] +fn test_sha_multiple_files(algo: &str, len: u32) { + sha_cmd(algo, len) + .arg("lorem_ipsum.txt") + .arg("alice_in_wonderland.txt") + .succeeds() + .stdout_is_fixture(sha_fixture_name(algo, len, "", "_multiple_files.expected")); } -#[test] -fn test_untagged_sha2_single_file() { - for l in SHA_LENGTHS { - new_ucmd!() - .arg("--untagged") - .arg("--algorithm=sha2") - .arg(format!("--length={l}")) - .arg("lorem_ipsum.txt") - .succeeds() - .stdout_is_fixture(format!("untagged/sha{l}_single_file.expected")); - } +#[apply(test_sha)] +fn test_sha_stdin(algo: &str, len: u32) { + sha_cmd(algo, len) + .pipe_in_fixture("lorem_ipsum.txt") + .succeeds() + .stdout_is_fixture(sha_fixture_name(algo, len, "", "_stdin.expected")); } -#[test] -fn test_untagged_sha2_multiple_files() { - for l in SHA_LENGTHS { - new_ucmd!() - .arg("--untagged") - .arg("--algorithm=sha2") - .arg(format!("--length={l}")) - .arg("lorem_ipsum.txt") - .arg("alice_in_wonderland.txt") - .succeeds() - .stdout_is_fixture(format!("untagged/sha{l}_multiple_files.expected")); - } +#[apply(test_sha)] +fn test_untagged_sha_single_file(algo: &str, len: u32) { + sha_cmd(algo, len) + .arg("--untagged") + .arg("lorem_ipsum.txt") + .succeeds() + .stdout_is_fixture(sha_fixture_name( + algo, + len, + "untagged/", + "_single_file.expected", + )); } -#[test] -fn test_untagged_sha2_stdin() { - for l in SHA_LENGTHS { - new_ucmd!() - .arg("--untagged") - .arg("--algorithm=sha2") - .arg(format!("--length={l}")) - .pipe_in_fixture("lorem_ipsum.txt") - .succeeds() - .stdout_is_fixture(format!("untagged/sha{l}_stdin.expected")); - } +#[apply(test_sha)] +fn test_untagged_sha_multiple_files(algo: &str, len: u32) { + sha_cmd(algo, len) + .arg("--untagged") + .arg("lorem_ipsum.txt") + .arg("alice_in_wonderland.txt") + .succeeds() + .stdout_is_fixture(sha_fixture_name( + algo, + len, + "untagged/", + "_multiple_files.expected", + )); } -#[test] -fn test_check_tagged_sha2_single_file() { - for l in SHA_LENGTHS { - new_ucmd!() - .arg("--check") - .arg(format!("sha{l}_single_file.expected")) - .succeeds() - .stdout_is("lorem_ipsum.txt: OK\n"); - } +#[apply(test_sha)] +fn test_untagged_sha_stdin(algo: &str, len: u32) { + sha_cmd(algo, len) + .arg("--untagged") + .pipe_in_fixture("lorem_ipsum.txt") + .succeeds() + .stdout_is_fixture(sha_fixture_name(algo, len, "untagged/", "_stdin.expected")); } -#[test] -fn test_check_tagged_sha2_multiple_files() { - for l in SHA_LENGTHS { - new_ucmd!() - .arg("--check") - .arg(format!("sha{l}_multiple_files.expected")) - .succeeds() - .stdout_contains("lorem_ipsum.txt: OK\n") - .stdout_contains("alice_in_wonderland.txt: OK\n"); - } +#[apply(test_sha)] +fn test_check_tagged_sha_single_file(algo: &str, len: u32) { + new_ucmd!() + .arg("--check") + .arg(sha_fixture_name(algo, len, "", "_single_file.expected")) + .succeeds() + .stdout_is("lorem_ipsum.txt: OK\n"); } -// When checking sha2 in untagged mode, the length is automatically deduced -// from the length of the digest. -#[test] -fn test_check_untagged_sha2_single_file() { - for l in SHA_LENGTHS { - new_ucmd!() - .arg("--check") - .arg("--algorithm=sha2") - .arg(format!("untagged/sha{l}_single_file.expected")) - .succeeds() - .stdout_is("lorem_ipsum.txt: OK\n"); - } +#[apply(test_sha)] +fn test_check_tagged_sha_multiple_files(algo: &str, len: u32) { + new_ucmd!() + .arg("--check") + .arg(sha_fixture_name(algo, len, "", "_multiple_files.expected")) + .succeeds() + .stdout_contains("lorem_ipsum.txt: OK\n") + .stdout_contains("alice_in_wonderland.txt: OK\n"); } -#[test] -fn test_check_untagged_sha2_multiple_files() { - for l in SHA_LENGTHS { - new_ucmd!() - .arg("--check") - .arg("--algorithm=sha2") - .arg(format!("untagged/sha{l}_multiple_files.expected")) - .succeeds() - .stdout_contains("lorem_ipsum.txt: OK\n") - .stdout_contains("alice_in_wonderland.txt: OK\n"); - } +// When checking sha2/sha3 in untagged mode, the length is automatically +// deduced from the length of the digest. +#[apply(test_sha)] +fn test_check_untagged_sha_single_file(algo: &str, len: u32) { + new_ucmd!() + .arg("--check") + .arg(format!("--algorithm={algo}")) + .arg(sha_fixture_name( + algo, + len, + "untagged/", + "_single_file.expected", + )) + .succeeds() + .stdout_is("lorem_ipsum.txt: OK\n"); +} + +#[apply(test_sha)] +fn test_check_untagged_sha_multiple_files(algo: &str, len: u32) { + new_ucmd!() + .arg("--check") + .arg(format!("--algorithm={algo}")) + .arg(sha_fixture_name( + algo, + len, + "untagged/", + "_multiple_files.expected", + )) + .succeeds() + .stdout_contains("lorem_ipsum.txt: OK\n") + .stdout_contains("alice_in_wonderland.txt: OK\n"); +} + +#[rstest] +#[case::sha2("sha2", &[ + "d14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b", + "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e" +])] +#[case::sha3("sha3", &[ + "6b4e03423667dbb73b6e15454f0eb1abd4597f9a1b078e3f5b5a6bc7", + "a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a", + "0c63a75b845e4f7d01107d852e4c2485c51a50aaaa94fc61995e71bbee983a2ac3713831264adb47fb6bd1e058d5f004", + "a69f73cca23a9ac5c8b567dc185a756e97c982164fe25859e0d1dcc1475c80a615b2123af1f5f94c11e3e9402c3ac558f500199d95b6d3e301758586281dcd26" +])] +fn test_check_untagged_sha_invalid_length(#[case] algo: &str, #[case] digests: &[&str; 4]) { + // When checking with --algorithm sha2/sha3, Guess the length from the provided + // digest. Raise "improperly formatted" if the digest is not af adequate + // size (224, 256, 384 or 512 bits). + + let (at, mut ucmd) = at_and_ucmd!(); + at.touch("a"); + at.touch("b"); + at.touch("c"); + at.touch("d"); + at.touch("e"); + + let invalid = "xxxx e"; + + ucmd.arg("-a") + .arg(algo) + .arg("-c") + .pipe_in(format!( + "{} a\n{} b\n{} c\n{} d\n{invalid}", + digests[0], digests[1], digests[2], digests[3] + )) + .succeeds() + .stdout_contains("a: OK") + .stdout_contains("b: OK") + .stdout_contains("c: OK") + .stdout_contains("d: OK") + .stdout_does_not_contain("e: FAILED") + .stderr_contains("improperly formatted"); } #[test] @@ -555,130 +606,70 @@ fn test_check_sha2_tagged_variant() { } #[test] -fn test_sha3_single_file() { - for l in SHA_LENGTHS { - new_ucmd!() - .arg("--algorithm=sha3") - .arg(format!("--length={l}")) - .arg("lorem_ipsum.txt") - .succeeds() - .stdout_is_fixture(format!("sha3_{l}_single_file.expected")); - } -} +fn test_check_sha2_tagged_missing_hint() { + // SHA2 () = + // + // should fail and raise "improperly formatted" because SHA2 expects a + // mandatory length hint. -#[test] -fn test_sha3_multiple_files() { - for l in SHA_LENGTHS { - new_ucmd!() - .arg("--algorithm=sha3") - .arg(format!("--length={l}")) - .arg("lorem_ipsum.txt") - .arg("alice_in_wonderland.txt") - .succeeds() - .stdout_is_fixture(format!("sha3_{l}_multiple_files.expected")); - } -} - -#[test] -fn test_sha3_stdin() { - for l in SHA_LENGTHS { - new_ucmd!() - .arg("--algorithm=sha3") - .arg(format!("--length={l}")) - .pipe_in_fixture("lorem_ipsum.txt") - .succeeds() - .stdout_is_fixture(format!("sha3_{l}_stdin.expected")); - } -} + let (at, mut ucmd) = at_and_ucmd!(); + at.touch("a"); + at.touch("b"); -#[test] -fn test_untagged_sha3_single_file() { - for l in SHA_LENGTHS { - new_ucmd!() - .arg("--untagged") - .arg("--algorithm=sha3") - .arg(format!("--length={l}")) - .arg("lorem_ipsum.txt") - .succeeds() - .stdout_is_fixture(format!("untagged/sha3_{l}_single_file.expected")); - } -} + // valid digest but missing hint + let invalid_sha224 = "SHA2 (a) = d14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f"; + // invalid digest + let invalid = "SHA2 (b) = xxxx"; -#[test] -fn test_untagged_sha3_multiple_files() { - for l in SHA_LENGTHS { - new_ucmd!() - .arg("--untagged") - .arg("--algorithm=sha3") - .arg(format!("--length={l}")) - .arg("lorem_ipsum.txt") - .arg("alice_in_wonderland.txt") - .succeeds() - .stdout_is_fixture(format!("untagged/sha3_{l}_multiple_files.expected")); - } + ucmd.arg("-c") + .pipe_in(format!("{invalid_sha224}\n{invalid}")) + .fails() + .stderr_contains("no properly formatted checksum lines found"); } -#[test] -fn test_untagged_sha3_stdin() { - for l in SHA_LENGTHS { - new_ucmd!() - .arg("--untagged") - .arg("--algorithm=sha3") - .arg(format!("--length={l}")) - .pipe_in_fixture("lorem_ipsum.txt") - .succeeds() - .stdout_is_fixture(format!("untagged/sha3_{l}_stdin.expected")); - } -} +#[rstest] +#[case::md5("md5", "d41d8cd98f00b204e9800998ecf8427e")] +#[case::sha1("sha1", "da39a3ee5e6b4b0d3255bfef95601890afd80709")] +#[case::sha224("sha224", "d14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f")] +#[case::sha256( + "sha256", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" +)] +#[case::sha384( + "sha384", + "38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b" +)] +#[case::sha512( + "sha512", + "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e" +)] +#[case::sm3( + "sm3", + "1ab21d8355cfa17f8e61194831e81a8f22bec8c728fefb747ed035eb5082aa2b" +)] +fn test_check_untagged_with_invalid_length(#[case] algo: &str, #[case] digest: &str) { + // issue #11202 -#[test] -fn test_check_tagged_sha3_single_file() { - for l in SHA_LENGTHS { - new_ucmd!() - .arg("--check") - .arg(format!("sha3_{l}_single_file.expected")) - .succeeds() - .stdout_is("lorem_ipsum.txt: OK\n"); - } -} + // Ensures that when checking untagged lines, if the provided algorithm is + // one of `sha(224|256|384|512)`, digests whose length mismatches the + // length of the given algorithm will report as "improperly formatted" + // rather than a FAILED check. -#[test] -fn test_check_tagged_sha3_multiple_files() { - for l in SHA_LENGTHS { - new_ucmd!() - .arg("--check") - .arg(format!("sha3_{l}_multiple_files.expected")) - .succeeds() - .stdout_contains("lorem_ipsum.txt: OK\n") - .stdout_contains("alice_in_wonderland.txt: OK\n"); - } -} + let (at, mut ucmd) = at_and_ucmd!(); + at.touch("a"); + at.touch("b"); -// When checking sha3 in untagged mode, the length is automatically deduced -// from the length of the digest. -#[test] -fn test_check_untagged_sha3_single_file() { - for l in SHA_LENGTHS { - new_ucmd!() - .arg("--check") - .arg("--algorithm=sha3") - .arg(format!("untagged/sha3_{l}_single_file.expected")) - .succeeds() - .stdout_is("lorem_ipsum.txt: OK\n"); - } -} + let good_checksum = format!("{digest} a"); + let invalid_checksum = "e3b0 b"; -#[test] -fn test_check_untagged_sha3_multiple_files() { - for l in SHA_LENGTHS { - new_ucmd!() - .arg("--check") - .arg("--algorithm=sha3") - .arg(format!("untagged/sha3_{l}_multiple_files.expected")) - .succeeds() - .stdout_contains("lorem_ipsum.txt: OK\n") - .stdout_contains("alice_in_wonderland.txt: OK\n"); - } + ucmd.arg("-a") + .arg(algo) + .arg("-c") + .pipe_in(format!("{invalid_checksum}\n{good_checksum}")) + .succeeds() + .stdout_contains("a: OK") + .stdout_does_not_contain("b: FAILED") + .stderr_contains("WARNING: 1 line is improperly formatted"); } #[test] @@ -845,18 +836,17 @@ fn test_blake2b_length_invalid() { } } -#[test] -fn test_raw_single_file() { - for algo in ALGOS { - new_ucmd!() - .arg("--raw") - .arg("lorem_ipsum.txt") - .arg(format!("--algorithm={algo}")) - .succeeds() - .no_stderr() - .stdout_is_fixture_bytes(format!("raw/{algo}_single_file.expected")); - } +#[apply(test_all_algos)] +fn test_raw_single_file(#[case] algo: &str) { + new_ucmd!() + .arg("--raw") + .arg("lorem_ipsum.txt") + .arg(format!("--algorithm={algo}")) + .succeeds() + .no_stderr() + .stdout_is_fixture_bytes(format!("raw/{algo}_single_file.expected")); } + #[test] fn test_raw_multiple_files() { new_ucmd!() @@ -881,18 +871,17 @@ fn test_base64_raw_conflicts() { .stderr_contains("--raw"); } -#[test] -fn test_base64_single_file() { - for algo in ALGOS { - new_ucmd!() - .arg("--base64") - .arg("lorem_ipsum.txt") - .arg(format!("--algorithm={algo}")) - .succeeds() - .no_stderr() - .stdout_is_fixture_bytes(format!("base64/{algo}_single_file.expected")); - } +#[apply(test_all_algos)] +fn test_base64_single_file(#[case] algo: &str) { + new_ucmd!() + .arg("--base64") + .arg("lorem_ipsum.txt") + .arg(format!("--algorithm={algo}")) + .succeeds() + .no_stderr() + .stdout_is_fixture_bytes(format!("base64/{algo}_single_file.expected")); } + #[test] fn test_base64_multiple_files() { new_ucmd!() @@ -918,8 +907,8 @@ fn test_fail_on_folder() { .stderr_contains(format!("cksum: {folder_name}: Is a directory")); } -#[test] -fn test_all_algorithms_fail_on_folder() { +#[apply(test_all_algos)] +fn test_all_algorithms_fail_on_folder(#[case] algo: &str) { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; @@ -927,15 +916,13 @@ fn test_all_algorithms_fail_on_folder() { let folder_name = "a_folder"; at.mkdir(folder_name); - for algo in ALGOS { - scene - .ucmd() - .arg(format!("--algorithm={algo}")) - .arg(folder_name) - .fails() - .no_stdout() - .stderr_contains(format!("cksum: {folder_name}: Is a directory")); - } + scene + .ucmd() + .arg(format!("--algorithm={algo}")) + .arg(folder_name) + .fails() + .no_stdout() + .stderr_contains(format!("cksum: {folder_name}: Is a directory")); } #[cfg(unix)] @@ -970,7 +957,7 @@ fn test_dev_null() { #[cfg(unix)] #[test] -fn test_blake2b_512() { +fn test_blake2b_default_length() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; @@ -2057,6 +2044,28 @@ fn test_check_blake_length_guess() { .arg(at.subdir.join("foo.sums")) .fails() .stderr_contains("foo.sums: no properly formatted checksum lines found"); + + // This is incorrect because the length hint provided doesn't match the + // checksum length. + let length_mismatch = "BLAKE2b-8 (foo.dat) = 171cdfdf84ed"; + at.write("foo.sums", length_mismatch); + scene + .ucmd() + .arg("--check") + .arg(at.subdir.join("foo.sums")) + .fails() + .stderr_contains("foo.sums: no properly formatted checksum lines found"); + + // In this case, validation should fail because even though the checksum + // length matches the hint, it is above BLAKE2b's max (512). + let too_long = "BLAKE2b-520 (/dev/null) = 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; + at.write("foo.sums", too_long); + scene + .ucmd() + .arg("--check") + .arg(at.subdir.join("foo.sums")) + .fails() + .stderr_contains("foo.sums: no properly formatted checksum lines found"); } #[test] @@ -3287,3 +3296,112 @@ fn test_check_shake256_no_length() { .fails() .stderr_only("cksum: 'standard input': no properly formatted checksum lines found\n"); } + +#[template] +#[rstest] +#[case::no_length( + b"foo", + "04e0bb39f30b1a3feb89f536c93be15055482df748674b00d26e5a75777702e9", + None +)] +#[case( + b"foo", + "04e0bb39f30b1a3feb89f536c93be15055482df748674b00d26e5a75777702e9", + Some(0) +)] +#[case( + b"foo", + "04e0bb39f30b1a3feb89f536c93be15055482df748674b00d26e5a75777702e9", + Some(256) +)] +#[case( + b"foo", + "04e0bb39f30b1a3feb89f536c93be15055482df748674b00d26e5a75777702e9791074b7511b59d31c71c62f5a745689fa6c", + Some(400) +)] +#[case(b"foo", "04e0bb39f3", Some(40))] +#[case(b"foo", "04e0", Some(16))] +#[case(b"foo", "04", Some(8))] +fn test_blake3(#[case] input: &[u8], #[case] expected: &str, #[case] length: Option) {} + +#[apply(test_blake3)] +fn test_compute_blake3( + #[case] input: &[u8], + #[case] expected: &str, + #[case] length: Option, +) { + let length_args: &[String] = if let Some(len) = length { + &["-l".into(), len.to_string()] + } else { + &[] + }; + + new_ucmd!() + .arg("-a") + .arg("blake3") + .args(length_args) + .pipe_in(input) + .succeeds() + .stdout_only(format!( + "BLAKE3{} (-) = {expected}\n", + match length { + Some(0) | None => "-256".into(), + Some(i) => format!("-{i}"), + } + )); + + // with --untagged + new_ucmd!() + .arg("-a") + .arg("blake3") + .arg("--untagged") + .args(length_args) + .pipe_in(input) + .succeeds() + .stdout_only(format!("{expected} -\n")); +} + +#[apply(test_blake3)] +fn test_check_blake3_tagged( + #[case] input: &[u8], + #[case] digest: &str, + #[case] opt_len: Option, +) { + let (at, mut ucmd) = at_and_ucmd!(); + at.write_bytes("FILE", input); + + let len = match opt_len { + Some(0) => "-256".into(), + Some(i) => format!("-{i}"), + None => String::new(), + }; + + let tagged = format!("BLAKE3{len} (FILE) = {digest}",); + + ucmd.arg("-c") + .arg("-a") + .arg("blake3") + .pipe_in(tagged) + .succeeds() + .stdout_only("FILE: OK\n"); +} + +#[apply(test_blake3)] +#[allow(clippy::used_underscore_binding)] +fn test_check_blake3_untagged( + #[case] input: &[u8], + #[case] digest: &str, + #[case] _opt_len: Option, +) { + let (at, mut ucmd) = at_and_ucmd!(); + at.write_bytes("FILE", input); + + let untagged = format!("{digest} FILE"); + + ucmd.arg("-c") + .arg("-a") + .arg("blake3") + .pipe_in(untagged) + .succeeds() + .stdout_only("FILE: OK\n"); +} diff --git a/tests/by-util/test_cut.rs b/tests/by-util/test_cut.rs index abb817b1400..160ad59c878 100644 --- a/tests/by-util/test_cut.rs +++ b/tests/by-util/test_cut.rs @@ -229,6 +229,21 @@ fn test_zero_terminated_only_delimited() { .stdout_only("82\n7\0"); } +#[test] +fn test_suppresses_unterminated_segment() { + new_ucmd!() + .args(&["-z", "-d", "", "-s", "-f", "1"]) + .pipe_in("unterminated") + .succeeds() + .stdout_only_bytes(""); + + new_ucmd!() + .args(&["-z", "-d", "", "-s", "-f", "1"]) + .pipe_in("terminated\0unterminated") + .succeeds() + .stdout_only_bytes("terminated\0"); +} + #[test] fn test_is_a_directory() { let (at, mut ucmd) = at_and_ucmd!(); diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index db4637e6273..7096e2040ee 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -3,7 +3,8 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. // -// spell-checker: ignore: AEDT AEST EEST NZDT NZST Kolkata Iseconds févr février janv janvier mercredi samedi sommes juin décembre Januar Juni Dezember enero junio diciembre gennaio giugno dicembre junho dezembro lundi dimanche Montag Sonntag Samstag sábado febr MEST KST +// spell-checker: ignore: AEDT AEST EEST NZDT NZST Kolkata Iseconds févr février janv janvier mercredi samedi sommes juin décembre Januar Juni Dezember enero junio diciembre gennaio giugno dicembre junho dezembro lundi dimanche Montag Sonntag Samstag sábado febr MEST KST uueuu ueuu vasárnap június január distros +// spell-checker: ignore: uppercases use std::cmp::Ordering; @@ -13,6 +14,8 @@ use regex::Regex; #[cfg(all(unix, not(target_os = "macos")))] use uucore::process::geteuid; use uutests::util::TestScenario; +#[cfg(unix)] +use uutests::util::is_locale_available; use uutests::{at_and_ucmd, new_ucmd, util_name}; #[test] @@ -1619,6 +1622,32 @@ fn test_date_locale_en_us_vs_c_difference() { } } +#[test] +#[cfg(unix)] +fn test_date_locale_hu_hungarian() { + // Regression test for uutils/coreutils#11240: the GNU modifier fast-path + // ("%-e") used to run before ICU localization, so "%b"/"%A" came out in + // English even under hu_HU.UTF-8. Pin an explicit format string so the + // assertion is deterministic across glibc versions (the default D_T_FMT + // for hu_HU differs between distros). + if !is_locale_available("hu_HU.UTF-8") { + return; + } + + let result = new_ucmd!() + .env("LC_ALL", "hu_HU.UTF-8") + .env("TZ", "UTC") + .arg("-d") + .arg("2025-12-14T13:00:00") + .arg("+%Y. %b %-e., %A, %H:%M:%S %Z") + .succeeds(); + + assert_eq!( + result.stdout_str(), + "2025. dec 14., vasárnap, 13:00:00 UTC\n" + ); +} + #[test] #[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))] fn test_date_locale_fr_french() { @@ -1823,6 +1852,166 @@ fn test_date_parenthesis_comment() { } } +#[test] +fn test_date_strftime_narrow_width_on_wide_default() { + // `%j` has a default width of 3. Requesting `%02j` on day 1 should yield `01`. + // uutils currently yields `1`. + new_ucmd!() + .env("LC_ALL", "C") + .env("TZ", "UTC") + .arg("-d") + .arg("2024-01-01") + .arg("+%02j") + .succeeds() + .stdout_is("01\n"); +} + +#[test] +#[ignore = "https://github.com/uutils/parse_datetime/issues/283 — GNU date floors negative fractional epochs (`@-1.5` -> -2); uutils truncates toward zero (-> -1)."] +fn test_date_negative_fractional_epoch_flooring() { + new_ucmd!() + .env("LC_ALL", "C") + .env("TZ", "UTC") + .arg("-d") + .arg("@-1.5") + .arg("+%s") + .succeeds() + .stdout_is("-2\n"); +} + +#[test] +#[ignore = "https://github.com/uutils/parse_datetime/issues/282 — parse_datetime rejects `HH:MM am/pm` forms (e.g. `2024-06-15 12:00 PM`, `2024-06-15 11:30am`). GNU date accepts them."] +fn test_date_input_hhmm_ampm() { + for input in [ + "2024-06-15 12:00 PM", + "2024-06-15 11:30am", + "2024-06-15 3:00 PM", + ] { + new_ucmd!() + .env("LC_ALL", "C") + .env("TZ", "UTC") + .arg("-d") + .arg(input) + .arg("+%H:%M") + .succeeds(); + } +} + +#[test] +#[ignore = "https://github.com/uutils/parse_datetime/issues/281 — GNU date re-zones input with trailing TZ abbreviation (e.g. `2024-01-01 EST`) into the local TZ; uutils keeps the input TZ on output."] +fn test_date_input_trailing_tz_abbrev_rezones() { + // `TZ=UTC+1 date -d '2024-01-01 EST'` should display the instant in UTC+1 + // (GNU: 04:00:00 UTC), not leave it in EST (uutils: 00:00:00 -05). + new_ucmd!() + .env("LC_ALL", "C") + .env("TZ", "UTC+1") + .arg("-d") + .arg("2024-01-01 EST") + .arg("+%H:%M:%S %:z") + .succeeds() + .stdout_is("04:00:00 -01:00\n"); +} + +#[test] +fn test_date_strftime_case_flag_on_alt_ampm() { + // `%P` is GNU's lowercase am/pm. `%#P` should stay lowercase in GNU; uutils flips to `PM`. + new_ucmd!() + .env("LC_ALL", "C") + .env("TZ", "UTC") + .arg("-d") + .arg("2024-06-15 13:45:30") + .arg("+%#P") + .succeeds() + .stdout_is("pm\n"); +} + +#[test] +#[ignore = "https://github.com/uutils/coreutils/issues/11658 — GNU date applies flags/widths to `%N` (nanoseconds); uutils ignores/mishandles them."] +fn test_date_strftime_n_width_and_flags() { + // `%_3N` should space-pad nanoseconds to width 3. GNU outputs `0 `; uutils outputs `0`. + new_ucmd!() + .env("LC_ALL", "C") + .env("TZ", "UTC") + .arg("-d") + .arg("@0") + .arg("+%_3N") + .succeeds() + .stdout_is("0 \n"); + // `%-N` (no-padding flag) should still output the full 9-digit default. + // GNU: `000000000`; uutils: `0`. + new_ucmd!() + .env("LC_ALL", "C") + .env("TZ", "UTC") + .arg("-d") + .arg("@0") + .arg("+%-N") + .succeeds() + .stdout_is("000000000\n"); +} + +#[test] +#[ignore = "https://github.com/uutils/coreutils/issues/11657 — GNU date treats composite strftime specifiers (%D, %F, %T, ...) as atomic; flags like `-` should not propagate to sub-fields."] +fn test_date_strftime_flag_on_composite() { + // GNU `%-D` keeps `06/15/24` (flag ignored on composite). + // uutils applies `-` to inner `%m`, producing `6/15/24`. + new_ucmd!() + .env("LC_ALL", "C") + .env("TZ", "UTC") + .arg("-d") + .arg("2024-06-15") + .arg("+%-D") + .succeeds() + .stdout_is("06/15/24\n"); +} + +#[test] +#[ignore = "https://github.com/uutils/coreutils/issues/11656 — GNU date strips the `O` strftime modifier in C locale (e.g. `%Om` -> `%m`); uutils leaks it as literal `%om`."] +fn test_date_strftime_o_modifier() { + // In C locale the `O` modifier is a no-op (alternative numeric symbols). + // GNU renders `%Om` as `06` for June; uutils renders it as the literal `%Om`. + new_ucmd!() + .env("LC_ALL", "C") + .env("TZ", "UTC") + .arg("-d") + .arg("2024-06-15") + .arg("+%Om-%Oy-%Ol") + .succeeds() + .stdout_is("06-24-12\n"); +} + +#[test] +#[ignore = "https://github.com/uutils/parse_datetime/issues/280 — GNU date accepts bare timezone abbreviations (UT, GMT, ...) meaning `now in that TZ`; parse_datetime rejects them."] +fn test_date_bare_timezone_abbreviation() { + // GNU: `date -d ut`, `date -d UT`, `date -d gmt` → current time in UTC. + // uutils: "invalid date" error. + for input in ["ut", "UT", "gmt", "GMT"] { + new_ucmd!() + .env("TZ", "UTC+1") + .arg("-d") + .arg(input) + .arg("+%Z") + .succeeds() + .stdout_is("UTC\n"); + } +} + +#[test] +#[ignore = "https://github.com/uutils/parse_datetime/issues/279 — GNU date silently ignores unrecognized trailing tokens (e.g. `8j`), but parse_datetime rejects them."] +fn test_date_ignores_unrecognized_trailing_tokens() { + // GNU compatibility: trailing unknown word-tokens after a valid number are ignored. + // GNU parses `8j`, `8 j`, etc. as hour 8; our parse_datetime crate errors out. + for input in ["8j", "8 j"] { + new_ucmd!() + .env("TZ", "UTC") + .arg("-u") + .arg("-d") + .arg(input) + .arg("+%H:%M:%S") + .succeeds() + .stdout_only("08:00:00\n"); + } +} + #[test] fn test_date_parenthesis_vs_other_special_chars() { // Ensure parentheses are special but other chars like [, ., ^ are still rejected @@ -1840,20 +2029,7 @@ fn test_date_parenthesis_vs_other_special_chars() { fn test_date_iranian_locale_solar_hijri_calendar() { // Test Iranian locale uses Solar Hijri calendar // Verify the Solar Hijri calendar is used in the Iranian locale - use std::process::Command; - - // Check if Iranian locale is available - let locale_check = Command::new("locale") - .env("LC_ALL", "fa_IR.UTF-8") - .arg("charmap") - .output(); - - let locale_available = match locale_check { - Ok(output) => String::from_utf8_lossy(&output.stdout).trim() == "UTF-8", - Err(_) => false, - }; - - if !locale_available { + if !is_locale_available("fa_IR.UTF-8") { println!("Skipping Iranian locale test - fa_IR.UTF-8 locale not available"); return; } @@ -1920,20 +2096,7 @@ fn test_date_iranian_locale_solar_hijri_calendar() { fn test_date_ethiopian_locale_calendar() { // Test Ethiopian locale uses Ethiopian calendar // Verify the Ethiopian calendar is used in the Ethiopian locale - use std::process::Command; - - // Check if Ethiopian locale is available - let locale_check = Command::new("locale") - .env("LC_ALL", "am_ET.UTF-8") - .arg("charmap") - .output(); - - let locale_available = match locale_check { - Ok(output) => String::from_utf8_lossy(&output.stdout).trim() == "UTF-8", - Err(_) => false, - }; - - if !locale_available { + if !is_locale_available("am_ET.UTF-8") { println!("Skipping Ethiopian locale test - am_ET.UTF-8 locale not available"); return; } @@ -2000,20 +2163,7 @@ fn test_date_ethiopian_locale_calendar() { fn test_date_thai_locale_solar_calendar() { // Test Thai locale uses Thai solar calendar // Verify the Thai solar calendar is used with the Thai locale - use std::process::Command; - - // Check if Thai locale is available - let locale_check = Command::new("locale") - .env("LC_ALL", "th_TH.UTF-8") - .arg("charmap") - .output(); - - let locale_available = match locale_check { - Ok(output) => String::from_utf8_lossy(&output.stdout).trim() == "UTF-8", - Err(_) => false, - }; - - if !locale_available { + if !is_locale_available("th_TH.UTF-8") { println!("Skipping Thai locale test - th_TH.UTF-8 locale not available"); return; } @@ -2149,6 +2299,7 @@ fn test_locale_month_names() { ("es_ES.UTF-8", "enero", "junio", "diciembre"), ("it_IT.UTF-8", "gennaio", "giugno", "dicembre"), ("pt_BR.UTF-8", "janeiro", "junho", "dezembro"), + ("hu_HU.UTF-8", "január", "június", "december"), ("ja_JP.UTF-8", "1月", "6月", "12月"), ("zh_CN.UTF-8", "一月", "六月", "十二月"), ] { @@ -2424,6 +2575,47 @@ fn test_date_format_modifier_percent_escape() { .stdout_is("%Y=0000001999\n"); } +#[test] +fn test_date_format_modifier_huge_width_fails_without_abort() { + // GNU date also exits with failure for extremely large width. + // Assert exit code only to avoid coupling to implementation-specific error text. + let format = format!("+%{}c", usize::MAX); + new_ucmd!().arg(&format).fails().code_is(1); +} + +#[test] +fn test_date_format_large_width_no_oom() { + // Regression: very large width like %8888888888r caused OOM. + // GNU caps width to i32::MAX; verify we don't crash. + // Use a moderate width with a fixed date to check the code path works. + new_ucmd!() + .arg("-d") + .arg("2024-01-01") + .arg("+%300S") + .succeeds() + .stdout_is(format!("{}\n", format_args!("{:0>300}", "00"))); + + // Test with a larger width to exercise the code path without producing + // gigabytes of output (the original %8888888888r would produce ~2GB). + new_ucmd!() + .arg("-d") + .arg("2024-01-01") + .arg("+%10000S") + .succeeds() + .stdout_is(format!("{}\n", format_args!("{:0>10000}", "00"))); + + // Mixed literal text with multiple width-modified specifiers. + // 2024-01-01 is Monday (day-of-week 1). + // %2u → "01", literal "ueuu", %6666u → "1" zero-padded to 6666, literal "-r". + let expected = format!("01ueuu{}-r\n", format_args!("{:0>6666}", "1")); + new_ucmd!() + .arg("-d") + .arg("2024-01-01") + .arg("+%2uueuu%6666u-r") + .succeeds() + .stdout_is(expected); +} + // Tests for format modifier edge cases (flags without explicit width) #[test] fn test_date_format_modifier_edge_cases() { diff --git a/tests/by-util/test_dd.rs b/tests/by-util/test_dd.rs index 7fe8886fbe8..e9574bb0549 100644 --- a/tests/by-util/test_dd.rs +++ b/tests/by-util/test_dd.rs @@ -115,6 +115,14 @@ fn help() { new_ucmd!().args(&["--help"]).succeeds(); } +#[test] +fn test_out_of_memory() { + new_ucmd!() + .arg("bs=1PB") + .fails_with_code(1) + .stderr_contains("memory"); //todo: improve error message at all platforms +} + #[test] fn test_stdin_stdout() { let input = build_ascii_block(521); @@ -255,6 +263,13 @@ fn test_zero_multiplier_warning() { .succeeds() .no_stdout() .stderr_contains("warning: '0x' is a zero multiplier; use '00x' if that is intended"); + + new_ucmd!() + .args(&[format!("{arg}=0x0x0").as_str(), "status=none"]) + .pipe_in("") + .succeeds() + .no_stdout() + .stderr_is("dd: warning: '0x' is a zero multiplier; use '00x' if that is intended\ndd: warning: '0x' is a zero multiplier; use '00x' if that is intended\n"); } } diff --git a/tests/by-util/test_echo.rs b/tests/by-util/test_echo.rs index 3398708f176..c3c9400995d 100644 --- a/tests/by-util/test_echo.rs +++ b/tests/by-util/test_echo.rs @@ -825,4 +825,14 @@ fn test_emoji_output() { .args(&["🦀", "loves", "🚀", "and", "🌟"]) .succeeds() .stdout_only("🦀 loves 🚀 and 🌟\n"); + + new_ucmd!() + .arg("🍎,🍌,🍒,🥝") + .succeeds() + .stdout_only("🍎,🍌,🍒,🥝\n"); + + new_ucmd!() + .arg("こんにちは世界") + .succeeds() + .stdout_only("こんにちは世界\n"); } diff --git a/tests/by-util/test_expr.rs b/tests/by-util/test_expr.rs index ec1cf91598f..c95465af7a7 100644 --- a/tests/by-util/test_expr.rs +++ b/tests/by-util/test_expr.rs @@ -217,6 +217,29 @@ fn test_and() { new_ucmd!().args(&["", "&", ""]).fails().stdout_only("0\n"); } +#[test] +fn test_parenthesized_short_circuit_dead_branches() { + new_ucmd!() + .args(&["1", "|", "(", "1", "/", "0", ")"]) + .succeeds() + .stdout_only("1\n"); + + new_ucmd!() + .args(&["0", "&", "(", "1", "/", "0", ")"]) + .fails_with_code(1) + .stdout_only("0\n"); + + new_ucmd!() + .args(&["1", "|", "(", "0", "&", "(", "1", "/", "0", ")", ")"]) + .succeeds() + .stdout_only("1\n"); + + new_ucmd!() + .args(&["0", "&", "(", "1", "|", "(", "1", "/", "0", ")", ")"]) + .fails_with_code(1) + .stdout_only("0\n"); +} + #[test] fn test_length_fail() { new_ucmd!().args(&["length", "αbcdef", "1"]).fails(); @@ -580,11 +603,11 @@ fn test_num_str_comparison() { } #[test] -fn test_eager_evaluation() { +fn test_missing_closing_parenthesis_reports_syntax_error() { new_ucmd!() .args(&["(", "1", "/", "0"]) .fails() - .stderr_contains("division by zero"); + .stderr_contains("expecting ')' after '0'"); } #[test] diff --git a/tests/by-util/test_head.rs b/tests/by-util/test_head.rs index 2acc783eaff..d954aa9d918 100644 --- a/tests/by-util/test_head.rs +++ b/tests/by-util/test_head.rs @@ -451,6 +451,35 @@ fn test_all_but_last_bytes_large_file_piped() { .stdout_only_fixture(seq_19000_file_name); } +#[test] +fn test_all_but_last_lines_large_file_presume_input_pipe() { + // Validate print-all-but-last-n-lines on the non-seekable path for a large terminated input. + // This input shape forces repeated buffer reuse while some chunks end mid-line. + let scene = TestScenario::new(util_name!()); + let fixtures = &scene.fixtures; + + let input_file_name = "reused_line_chunks"; + let expected_output_file_name = "reused_line_chunks_elide_last"; + let line = "aaaaaa\n"; + let input_line_count: usize = 20_000; + + let mut input = String::with_capacity(line.len() * input_line_count); + for _ in 0..input_line_count { + input.push_str(line); + } + fixtures.write(input_file_name, &input); + fixtures.write( + expected_output_file_name, + &line.repeat(input_line_count - 1), + ); + + scene + .ucmd() + .args(&["---presume-input-pipe", "-n", "-1", input_file_name]) + .succeeds() + .stdout_only_fixture(expected_output_file_name); +} + #[test] fn test_all_but_last_lines_large_file() { // Create our fixtures on the fly. We need the input file to be at least double @@ -882,3 +911,11 @@ fn test_head_non_utf8_paths() { assert!(output.contains("line3")); } // Test that head handles non-UTF-8 file names without crashing + +#[test] +fn test_do_not_attempt_to_read_a_directory() { + new_ucmd!() + .arg(".") + .fails_with_code(1) + .stderr_contains("error reading '.'"); +} diff --git a/tests/by-util/test_ls.rs b/tests/by-util/test_ls.rs index 11365392ae1..fe056008d58 100644 --- a/tests/by-util/test_ls.rs +++ b/tests/by-util/test_ls.rs @@ -57,6 +57,20 @@ const COMMA_ARGS: &[&str] = &["-m", "--format=commas", "--for=commas"]; const COLUMN_ARGS: &[&str] = &["-C", "--format=columns", "--for=columns"]; +#[test] +#[cfg(unix)] +fn test_directory_in_file() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.touch("file"); + + scene + .ucmd() + .arg("file/missing") + .fails_with_code(2) + .stderr_is("ls: cannot access 'file/missing': Not a directory\n"); +} + #[test] fn test_invalid_flag() { new_ucmd!() @@ -7119,3 +7133,18 @@ fn test_ls_non_utf8_hidden() { scene.ucmd().succeeds().stdout_does_not_contain(".hidden"); } + +#[test] +#[cfg(target_os = "wasi")] +fn test_ls_a_dotdot_no_error_on_wasi() { + // On WASI the sandbox may block access to ".." at the preopened root. + // ls -a should still succeed and show ".." without an error message. + let scene = TestScenario::new(util_name!()); + scene + .ucmd() + .arg("-a") + .arg("-1") + .succeeds() + .stdout_contains("..") + .no_stderr(); +} diff --git a/tests/by-util/test_numfmt.rs b/tests/by-util/test_numfmt.rs index c9ff617c1e9..ae03c9ffd5e 100644 --- a/tests/by-util/test_numfmt.rs +++ b/tests/by-util/test_numfmt.rs @@ -316,6 +316,14 @@ fn test_should_report_invalid_number_with_interior_junk() { .stderr_is("numfmt: invalid suffix in input: '1x0K'\n"); } +#[test] +fn test_should_report_invalid_number_with_sign_after_decimal() { + new_ucmd!() + .args(&["--", "-0.-1"]) + .fails_with_code(2) + .stderr_is("numfmt: invalid number: '-0.-1'\n"); +} + #[test] fn test_should_skip_leading_space_from_stdin() { new_ucmd!() @@ -1105,6 +1113,14 @@ fn test_format_grouping_conflicts_with_to_option() { .stderr_contains("grouping cannot be combined with --to"); } +#[test] +fn test_grouping_conflicts_with_format_option() { + new_ucmd!() + .args(&["--format=%f", "--grouping"]) + .fails_with_code(1) + .stderr_contains("--grouping cannot be combined with --format"); +} + #[test] fn test_zero_terminated_command_line_args() { new_ucmd!() @@ -1200,6 +1216,68 @@ fn test_debug_warnings() { .succeeds() .stdout_is("4.0K\n") .stderr_is("numfmt: --header ignored with command-line input\n"); + + new_ucmd!() + .env("LC_ALL", "C") + .args(&["--debug", "--grouping", "--from=si", "4.0K"]) + .succeeds() + .stdout_is("4000\n") + .stderr_is("numfmt: grouping has no effect in this locale\n"); +} + +#[test] +fn test_debug_reports_failed_conversions_summary() { + new_ucmd!() + .args(&[ + "--invalid=fail", + "--debug", + "--to=si", + "1000", + "Foo", + "3000", + ]) + .fails_with_code(2) + .stdout_is("1.0k\nFoo\n3.0k\n") + .stderr_is( + "numfmt: invalid number: 'Foo'\nnumfmt: failed to convert some of the input numbers\n", + ); +} + +#[test] +fn test_invalid_fail_with_fields_does_not_duplicate_output() { + new_ucmd!() + .args(&["--invalid=fail", "--field=2", "--from=si", "--to=iec"]) + .pipe_in("A 1K x\nB Foo y\nC 3G z\n") + .fails_with_code(2) + .stdout_is("A 1000 x\nB Foo y\nC 2.8G z\n") + .stderr_is("numfmt: invalid number: 'Foo'\n"); +} + +#[test] +fn test_abort_with_fields_preserves_partial_output() { + new_ucmd!() + .args(&["--field=3", "--from=auto", "Hello 40M World 90G"]) + .fails_with_code(2) + .stdout_is("Hello 40M ") + .stderr_is("numfmt: invalid number: 'World'\n"); +} + +#[test] +fn test_rejects_malformed_number_forms() { + new_ucmd!() + .args(&["--from=si", "12.K"]) + .fails_with_code(2) + .stderr_contains("invalid number: '12.K'"); + + new_ucmd!() + .args(&["--from=si", "--delimiter=,", "12. 2"]) + .fails_with_code(2) + .stderr_contains("invalid number: '12. 2'"); + + new_ucmd!() + .arg("..1") + .fails_with_code(2) + .stderr_contains("invalid suffix in input: '..1'"); } #[test] @@ -1237,6 +1315,21 @@ fn test_empty_delimiter_multi_char_unit_separator() { .stdout_only("1000\n2000000\n3000000000\n"); } +#[test] +fn test_whitespace_mode_parses_custom_unit_separator_inputs() { + new_ucmd!() + .args(&["--from=iec", "--unit-separator=::"]) + .pipe_in("4::K\n") + .succeeds() + .stdout_only("4096\n"); + + new_ucmd!() + .args(&["--from=iec", "--unit-separator=\u{a0}"]) + .pipe_in("4\u{a0}K\n") + .succeeds() + .stdout_only("4096\n"); +} + #[test] fn test_empty_delimiter_whitespace_rejection() { new_ucmd!() @@ -1273,6 +1366,100 @@ fn test_null_byte_input_multiline() { .stdout_is("1000\n3000"); } +// https://github.com/uutils/coreutils/issues/11653 +// GNU rejects `-9923868` as an invalid short option (leading `-9`) and +// requires `--` separator; uutils accepts it as a negative positional number. +#[test] +#[ignore = "GNU compat: see uutils/coreutils#11653"] +fn test_negative_number_without_double_dash_gnu_compat_issue_11653() { + new_ucmd!() + .args(&["--to=iec", "-9923868"]) + .fails_with_code(1) + .stderr_contains("invalid option"); +} + +// https://github.com/uutils/coreutils/issues/11654 +// uutils parses large integers through f64, losing precision past 2^53. +#[test] +#[ignore = "GNU compat: see uutils/coreutils#11654"] +fn test_large_integer_precision_loss_issue_11654() { + new_ucmd!() + .args(&["--from=iec", "9153396227555392131"]) + .succeeds() + .stdout_is("9153396227555392131\n"); +} + +// https://github.com/uutils/coreutils/issues/11655 +// uutils accepts scientific notation (`1e9`, `5e-3`, ...); GNU rejects it +// as "invalid suffix in input". +#[test] +#[ignore = "GNU compat: see uutils/coreutils#11655"] +fn test_scientific_notation_rejected_by_gnu_issue_11655() { + new_ucmd!() + .arg("1e9") + .fails_with_code(2) + .stderr_contains("invalid suffix in input"); +} + +// https://github.com/uutils/coreutils/issues/11662 +// `--to=auto` is accepted at parse time by uutils then rejected at runtime +// with exit code 2; GNU rejects it in option parsing with exit code 1. +#[test] +#[ignore = "GNU compat: see uutils/coreutils#11662"] +fn test_to_auto_rejected_at_parse_time_issue_11662() { + new_ucmd!() + .args(&["--to=auto", "100"]) + .fails_with_code(1) + .stderr_contains("invalid argument 'auto' for '--to'"); +} + +// https://github.com/uutils/coreutils/issues/11663 +// `--from-unit` multiplication with fractional input rounds to an integer; +// GNU preserves the fractional digits. +#[test] +fn test_from_unit_fractional_precision_issue_11663() { + new_ucmd!() + .args(&["--from=iec", "--from-unit=959", "--", "-615484.454"]) + .succeeds() + .stdout_is("-590249591.386\n"); +} + +// https://github.com/uutils/coreutils/issues/11664 +// Zero-padded `--format` places padding zeros before the sign for negative +// numbers; GNU (and C printf) puts the sign first. +#[test] +#[ignore = "GNU compat: see uutils/coreutils#11664"] +fn test_zero_pad_sign_order_issue_11664() { + new_ucmd!() + .args(&["--from=none", "--format=%018.2f", "--", "-9869647"]) + .succeeds() + .stdout_is("-00000009869647.00\n"); +} + +// https://github.com/uutils/coreutils/issues/11666 +// `--to-unit=N` selects the output prefix based on the unscaled value +// instead of `value / N`. +#[test] +#[ignore = "GNU compat: see uutils/coreutils#11666"] +fn test_to_unit_prefix_selection_issue_11666() { + new_ucmd!() + .args(&["--to=iec-i", "--to-unit=885", "100000"]) + .succeeds() + .stdout_is("113\n"); +} + +// https://github.com/uutils/coreutils/issues/11667 +// `--format='%.0f'` with `--to=` still prints one fractional digit; +// the precision specifier `.0` is ignored. +#[test] +#[ignore = "GNU compat: see uutils/coreutils#11667"] +fn test_format_precision_zero_with_to_scale_issue_11667() { + new_ucmd!() + .args(&["--to=iec", "--format=%.0f", "5183776"]) + .succeeds() + .stdout_is("5M\n"); +} + #[test] fn test_invalid_utf8_input() { // 0xFF is invalid UTF-8 diff --git a/tests/by-util/test_od.rs b/tests/by-util/test_od.rs index 21cef8404ea..823f2d1dc51 100644 --- a/tests/by-util/test_od.rs +++ b/tests/by-util/test_od.rs @@ -223,6 +223,52 @@ fn test_bfloat16_compact() { .stdout_only(" 1 1\n"); } +#[test] +fn test_tf_default_is_double() { + let input: [u8; 8] = [0x00, 0x00, 0x80, 0x3f, 0x00, 0x00, 0x00, 0x40]; + let default_output = new_ucmd!() + .arg("--endian=little") + .arg("-An") + .arg("-tf") + .run_piped_stdin(&input[..]) + .success() + .stdout_str() + .to_string(); + + let explicit_double_output = new_ucmd!() + .arg("--endian=little") + .arg("-An") + .arg("-tfD") + .run_piped_stdin(&input[..]) + .success() + .stdout_str() + .to_string(); + + let explicit_float_output = new_ucmd!() + .arg("--endian=little") + .arg("-An") + .arg("-tfF") + .run_piped_stdin(&input[..]) + .success() + .stdout_str() + .to_string(); + + assert_eq!(default_output, explicit_double_output); + assert_ne!(default_output, explicit_float_output); +} + +#[test] +fn test_tf_explicit_float_still_uses_4_bytes() { + let input: [u8; 8] = [0x00, 0x00, 0x80, 0x3f, 0x00, 0x00, 0x00, 0x40]; + new_ucmd!() + .arg("--endian=little") + .arg("-An") + .arg("-tfF") + .run_piped_stdin(&input[..]) + .success() + .stdout_only(" 1.0000000 2.0000000\n"); +} + #[test] fn test_f16() { let input: [u8; 14] = [ diff --git a/tests/by-util/test_pathchk.rs b/tests/by-util/test_pathchk.rs index 85f5c09ea0b..47fb1e01d1b 100644 --- a/tests/by-util/test_pathchk.rs +++ b/tests/by-util/test_pathchk.rs @@ -4,9 +4,11 @@ // file that was distributed with this source code. #[cfg(target_os = "linux")] use std::os::unix::ffi::OsStringExt; +#[cfg(unix)] use uutests::new_ucmd; #[test] +#[cfg(unix)] fn test_no_args() { new_ucmd!() .fails() @@ -15,11 +17,13 @@ fn test_no_args() { } #[test] +#[cfg(unix)] fn test_invalid_arg() { new_ucmd!().arg("--definitely-invalid").fails_with_code(1); } #[test] +#[cfg(unix)] fn test_default_mode() { // accept some reasonable default new_ucmd!().args(&["dir/file"]).succeeds().no_stdout(); @@ -55,6 +59,7 @@ fn test_default_mode() { } #[test] +#[cfg(unix)] fn test_posix_mode() { // accept some reasonable default new_ucmd!().args(&["-p", "dir/file"]).succeeds().no_stdout(); @@ -79,6 +84,7 @@ fn test_posix_mode() { } #[test] +#[cfg(unix)] fn test_posix_special() { // accept some reasonable default new_ucmd!().args(&["-P", "dir/file"]).succeeds().no_stdout(); @@ -118,6 +124,7 @@ fn test_posix_special() { } #[test] +#[cfg(unix)] fn test_posix_all() { // accept some reasonable default new_ucmd!() diff --git a/tests/by-util/test_rm.rs b/tests/by-util/test_rm.rs index c2b77ee6d3d..efa4ede0bd7 100644 --- a/tests/by-util/test_rm.rs +++ b/tests/by-util/test_rm.rs @@ -7,6 +7,8 @@ use std::process::Stdio; +#[cfg(unix)] +use rlimit::Resource; use uutests::{at_and_ucmd, new_ucmd, util::TestScenario, util_name}; #[test] @@ -1377,3 +1379,29 @@ fn test_preserve_root_literal_root() { .stderr_contains("it is dangerous to operate recursively on '/'") .stderr_contains("use --no-preserve-root to override this failsafe"); } + +/// Test that "traversal failed" message is shown when readdir fails during +/// recursive removal (e.g., due to file descriptor exhaustion). +#[cfg(unix)] +#[test] +fn test_traversal_failed_on_readdir_error() { + let (at, mut ucmd) = at_and_ucmd!(); + at.mkdir_all("a/b"); + at.touch("a/b/file"); + + // Use a very low file descriptor limit so that dup() inside readdir + // fails with EMFILE ("Too many open files"), triggering the + // "traversal failed" error path. + let result = ucmd + .args(&["-rf", "a"]) + .limit(Resource::NOFILE, 5, 5) + .fails(); + result.stderr_contains("traversal failed"); + // musl libc uses "No file descriptors available" instead of glibc's + // "Too many open files" for EMFILE. + let stderr = result.stderr_str(); + assert!( + stderr.contains("Too many open files") || stderr.contains("No file descriptors available"), + "{stderr:?} does not contain expected EMFILE message" + ); +} diff --git a/tests/by-util/test_sort.rs b/tests/by-util/test_sort.rs index d92f6da003b..b0e7602fd7f 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 (linux) NOFILE dfgi +// spell-checker:ignore (words) ints (linux) NOFILE dfgi abmon avril #![allow(clippy::cast_possible_wrap)] use std::env; @@ -15,6 +15,8 @@ use std::time::Duration; use uutests::at_and_ucmd; use uutests::new_ucmd; use uutests::util::TestScenario; +#[cfg(unix)] +use uutests::util::is_locale_available; fn test_helper(file_name: &str, possible_args: &[&str]) { for args in possible_args { @@ -603,6 +605,191 @@ fn test_month_default2() { } } +/// Query the system for abbreviated month names via `locale abmon`. +/// Returns a vector of 12 month abbreviations in order (Jan..Dec), +/// or None if the command fails or returns unexpected output. +#[cfg(any(target_vendor = "apple", target_os = "openbsd"))] +fn get_system_abmon(locale: &str) -> Option> { + let output = Command::new("locale") + .env("LC_ALL", locale) + .arg("abmon") + .output() + .ok()?; + if !output.status.success() { + return None; + } + let text = String::from_utf8(output.stdout).ok()?; + let months: Vec = text + .trim() + .split(';') + .map(String::from) + .filter(|m| !m.is_empty()) + .collect(); + if months.len() == 12 { + Some(months) + } else { + None + } +} + +/// Build shuffled input and sorted expected output from month names. +#[cfg(any(target_vendor = "apple", target_os = "openbsd"))] +fn month_sort_input_expected(months: &[String]) -> (String, String) { + // Shuffled order: May, Dec, Jan, Jun, Feb, Mar, Apr, Jul, Aug, Sep, Oct, Nov + let shuffle_order = [4, 11, 0, 5, 1, 2, 3, 6, 7, 8, 9, 10]; + let input = shuffle_order + .iter() + .map(|&i| months[i].as_str()) + .collect::>() + .join("\n") + + "\n"; + let expected = months.join("\n") + "\n"; + (input, expected) +} + +#[test] +#[cfg(unix)] +fn test_month_sort_french_locale() { + let locale = "fr_FR.UTF-8"; + if !is_locale_available(locale) { + return; + } + // spell-checker:disable + // On macOS/OpenBSD, abbreviated month names vary across OS versions (different CLDR data), + // so we query the system dynamically. On other platforms, glibc values are stable. + #[cfg(any(target_vendor = "apple", target_os = "openbsd"))] + let (input, expected) = { + let Some(months) = get_system_abmon(locale) else { + return; + }; + month_sort_input_expected(&months) + }; + #[cfg(not(any(target_vendor = "apple", target_os = "openbsd")))] + let (input, expected) = ( + "mai\ndéc.\njanv.\njuin\nfévr.\nmars\navril\njuil.\naoût\nsept.\noct.\nnov.\n".to_string(), + "janv.\nfévr.\nmars\navril\nmai\njuin\njuil.\naoût\nsept.\noct.\nnov.\ndéc.\n".to_string(), + ); + // spell-checker:enable + new_ucmd!() + .env("LC_ALL", locale) + .arg("-M") + .pipe_in(input) + .succeeds() + .stdout_is(expected); +} + +#[test] +#[cfg(unix)] +fn test_month_sort_hungarian_locale() { + let locale = "hu_HU.UTF-8"; + if !is_locale_available(locale) { + return; + } + // spell-checker:disable + #[cfg(any(target_vendor = "apple", target_os = "openbsd"))] + let (input, expected) = { + let Some(months) = get_system_abmon(locale) else { + return; + }; + month_sort_input_expected(&months) + }; + #[cfg(not(any(target_vendor = "apple", target_os = "openbsd")))] + let (input, expected) = ( + "máj\ndec\njan\njún\nfebr\nmárc\nápr\njúl\naug\nszept\nokt\nnov\n".to_string(), + "jan\nfebr\nmárc\nápr\nmáj\njún\njúl\naug\nszept\nokt\nnov\ndec\n".to_string(), + ); + // spell-checker:enable + new_ucmd!() + .env("LC_ALL", locale) + .arg("-M") + .pipe_in(input) + .succeeds() + .stdout_is(expected); +} + +/// Test that embedded blanks in month names cause a non-match (GNU compat). +/// E.g. "av ril" should NOT match "avril" — GNU treats it as unknown. +#[test] +#[cfg(unix)] +fn test_month_sort_french_embedded_blanks() { + let locale = "fr_FR.UTF-8"; + if !is_locale_available(locale) { + return; + } + // spell-checker:disable + // Pick three locale months (indices 2=March, 3=April, 5=June) and verify + // that inserting blanks into April's name causes a non-match. + // On glibc these are "mars", "avril", "juin"; on other systems they vary. + #[cfg(any(target_vendor = "apple", target_os = "openbsd"))] + let months = { + let Some(m) = get_system_abmon(locale) else { + return; + }; + m + }; + #[cfg(not(any(target_vendor = "apple", target_os = "openbsd")))] + let months = vec![ + "janv.", "févr.", "mars", "avril", "mai", "juin", "juil.", "août", "sept.", "oct.", "nov.", + "déc.", + ] + .into_iter() + .map(String::from) + .collect::>(); + + let march = &months[2]; + let april = &months[3]; + let june = &months[5]; + + // Build a mangled version of April with embedded spaces: e.g. "avril" -> "av ril" + // Insert spaces after the second byte. + if april.len() < 3 { + // Month name too short to meaningfully insert blanks; skip. + return; + } + let mangled = format!("{} {}", &april[..2], &april[2..]); + + // Input: june, mangled-april, march + let input = format!("{june}\n{mangled}\n{march}\n"); + // Expected: mangled sorts as unknown (first), then march (3), then june (6) + let expected = format!("{mangled}\n{march}\n{june}\n"); + // spell-checker:enable + new_ucmd!() + .env("LC_ALL", locale) + .arg("-M") + .pipe_in(input) + .succeeds() + .stdout_is(expected); +} + +#[test] +#[cfg(unix)] +fn test_month_sort_japanese_locale() { + let locale = "ja_JP.UTF-8"; + if !is_locale_available(locale) { + return; + } + // On macOS/OpenBSD, abbreviated month names may differ, so query dynamically. + #[cfg(any(target_vendor = "apple", target_os = "openbsd"))] + let (input, expected) = { + let Some(months) = get_system_abmon(locale) else { + return; + }; + month_sort_input_expected(&months) + }; + // Japanese abbreviated months are numeric (1月..12月) on glibc + #[cfg(not(any(target_vendor = "apple", target_os = "openbsd")))] + let (input, expected) = ( + "5月\n12月\n1月\n6月\n2月\n3月\n4月\n7月\n8月\n9月\n10月\n11月\n".to_string(), + "1月\n2月\n3月\n4月\n5月\n6月\n7月\n8月\n9月\n10月\n11月\n12月\n".to_string(), + ); + new_ucmd!() + .env("LC_ALL", locale) + .arg("-M") + .pipe_in(input) + .succeeds() + .stdout_is(expected); +} + #[test] fn test_default_unsorted_ints2() { let input = "9\n1909888\n000\n1\n2"; @@ -1569,6 +1756,15 @@ fn test_files0_from_empty() { .stderr_only("sort: no input from 'file'\n"); } +#[test] +#[cfg(unix)] +fn test_files0_read_error() { + new_ucmd!() + .args(&["--files0-from", "."]) + .fails_with_code(2) + .stderr_only("sort: cannot read: .: Is a directory\n"); +} + #[cfg(target_os = "linux")] #[test] // Test for GNU tests/sort/sort-files0-from.pl "empty-non-regular" diff --git a/tests/by-util/test_split.rs b/tests/by-util/test_split.rs index 73402dfa7c2..aa7c73ff9b8 100644 --- a/tests/by-util/test_split.rs +++ b/tests/by-util/test_split.rs @@ -2021,61 +2021,74 @@ fn test_split_non_utf8_paths() { #[test] #[cfg(target_os = "linux")] -fn test_split_non_utf8_prefix() { - use std::os::unix::ffi::OsStrExt; +fn test_split_non_utf8_prefix_is_byte_preserving() { + use std::ffi::OsStr; + use std::os::unix::ffi::{OsStrExt, OsStringExt}; + let (at, mut ucmd) = at_and_ucmd!(); + at.write("input.txt", "AB"); - at.write("input.txt", "line1\nline2\nline3\nline4\n"); + let invalid_prefix_bytes = b"p\xFF"; + at.write("p�aa", "keep-aa"); + at.write("p�ab", "keep-ab"); - let prefix = std::ffi::OsStr::from_bytes(b"\xFF\xFE"); - ucmd.arg("input.txt").arg(prefix).succeeds(); + ucmd.args(&["-b", "1", "input.txt"]) + .arg(OsStr::from_bytes(invalid_prefix_bytes)) + .succeeds(); - // Check that split files were created (functionality works) - // The actual filename may be converted due to lossy conversion, but the command should succeed - let entries: Vec<_> = fs::read_dir(at.as_string()).unwrap().collect(); - let split_files = entries - .iter() - .filter_map(|e| e.as_ref().ok()) - .filter(|entry| { - let name = entry.file_name(); - let name_str = name.to_string_lossy(); - name_str.starts_with("�") || name_str.len() > 2 // split files should exist + let mut produced_split_files: Vec> = fs::read_dir(at.as_string()) + .expect("temporary split directory should be readable") + .filter_map(|entry| { + let entry = entry.ok()?; + let name = entry.file_name().into_vec(); + if name.starts_with(invalid_prefix_bytes) { + Some(name) + } else { + None + } }) - .count(); - assert!( - split_files > 0, - "Expected at least one split file to be created" + .collect(); + produced_split_files.sort(); + + assert_eq!( + produced_split_files, + vec![b"p\xFFaa".to_vec(), b"p\xFFab".to_vec()] ); + assert_eq!(at.read("p�aa"), "keep-aa"); + assert_eq!(at.read("p�ab"), "keep-ab"); } #[test] #[cfg(target_os = "linux")] -fn test_split_non_utf8_additional_suffix() { - use std::os::unix::ffi::OsStrExt; - let (at, mut ucmd) = at_and_ucmd!(); +fn test_split_non_utf8_additional_suffix_is_byte_preserving() { + use std::ffi::OsStr; + use std::os::unix::ffi::{OsStrExt, OsStringExt}; - at.write("input.txt", "line1\nline2\nline3\nline4\n"); + let (at, mut ucmd) = at_and_ucmd!(); + at.write("input.txt", "AB"); - let suffix = std::ffi::OsStr::from_bytes(b"\xFF\xFE"); - ucmd.args(&["input.txt", "--additional-suffix"]) - .arg(suffix) + let suffix_bytes = b"\xFF\xFE"; + ucmd.args(&["-b", "1", "input.txt", "--additional-suffix"]) + .arg(OsStr::from_bytes(suffix_bytes)) .succeeds(); - // Check that split files were created (functionality works) - // The actual filename may be converted due to lossy conversion, but the command should succeed - let entries: Vec<_> = fs::read_dir(at.as_string()).unwrap().collect(); - let split_files = entries - .iter() - .filter_map(|e| e.as_ref().ok()) - .filter(|entry| { - let name = entry.file_name(); - let name_str = name.to_string_lossy(); - name_str.ends_with("�") || name_str.starts_with('x') // split files should exist + let mut produced_with_suffix: Vec> = fs::read_dir(at.as_string()) + .expect("temporary split directory should be readable") + .filter_map(|entry| { + let entry = entry.ok()?; + let name = entry.file_name().into_vec(); + if name.starts_with(b"xa") && name.ends_with(suffix_bytes) { + Some(name) + } else { + None + } }) - .count(); - assert!( - split_files > 0, - "Expected at least one split file to be created" + .collect(); + produced_with_suffix.sort(); + + assert_eq!( + produced_with_suffix, + vec![b"xaa\xFF\xFE".to_vec(), b"xab\xFF\xFE".to_vec()] ); } @@ -2089,5 +2102,5 @@ fn test_split_directory_already_exists() { ucmd.args(&["file"]) .fails_with_code(1) .no_stdout() - .stderr_is("split: xaa: Is a directory\n"); + .stderr_is("split: 'xaa': Is a directory\n"); } diff --git a/tests/by-util/test_sync.rs b/tests/by-util/test_sync.rs index 8c09d86372a..fdb93b72fd4 100644 --- a/tests/by-util/test_sync.rs +++ b/tests/by-util/test_sync.rs @@ -31,6 +31,11 @@ fn test_sync_fs() { .succeeds(); } +#[test] +fn test_sync_fs_without_files_falls_back_to_full_sync() { + new_ucmd!().arg("--file-system").succeeds(); +} + #[test] fn test_sync_data() { // Todo add a second arg diff --git a/tests/by-util/test_tee.rs b/tests/by-util/test_tee.rs index 8d03328b6f5..ae3e34dcd14 100644 --- a/tests/by-util/test_tee.rs +++ b/tests/by-util/test_tee.rs @@ -16,6 +16,15 @@ use std::time::Duration; // spell-checker:ignore nopipe +#[test] +#[cfg(unix)] +fn test_error_stdin_directory() { + new_ucmd!() + .set_stdin(std::fs::File::open(".").unwrap()) + .fails_with_code(1) + .stderr_is("tee: read error: Is a directory\n"); +} + #[test] fn test_invalid_arg() { new_ucmd!().arg("--definitely-invalid").fails_with_code(1); diff --git a/tests/by-util/test_timeout.rs b/tests/by-util/test_timeout.rs index 4bbc532e9cc..d6b0afb11d8 100644 --- a/tests/by-util/test_timeout.rs +++ b/tests/by-util/test_timeout.rs @@ -104,6 +104,13 @@ fn test_preserve_status() { } } +#[test] +fn test_kill_after_preserves_timeout_exit_without_preserve_status() { + new_ucmd!() + .args(&["-k", "1", "1", "sleep", "10"]) + .fails_with_code(124) + .no_output(); +} #[test] fn test_preserve_status_even_when_send_signal() { // When sending CONT signal, process doesn't get killed or stopped. @@ -286,3 +293,21 @@ fn test_foreground_signal0_kill_after() { .args(&["--foreground", "-s0", "-k.1", ".1", "sleep", "10"]) .fails_with_code(137); } + +#[test] +#[cfg(any(target_os = "linux", target_os = "android"))] +fn test_realtime_signal_names() { + // timeout should accept RTMIN and RTMAX as valid signal names + new_ucmd!() + .args(&["-v", "-s", "RTMAX", ".1", "sleep", "1"]) + .fails() + .stderr_contains("sending signal RTMAX to command"); + new_ucmd!() + .args(&["-v", "-s", "RTMIN", ".1", "sleep", "1"]) + .fails() + .stderr_contains("sending signal RTMIN to command"); + new_ucmd!() + .args(&["-v", "-s", "SIGRTMAX", ".1", "sleep", "1"]) + .fails() + .stderr_contains("sending signal RTMAX to command"); +} diff --git a/tests/by-util/test_tr.rs b/tests/by-util/test_tr.rs index fd4fc91a7fd..37f1af97212 100644 --- a/tests/by-util/test_tr.rs +++ b/tests/by-util/test_tr.rs @@ -65,6 +65,38 @@ fn test_delete() { .stdout_is("BD"); } +#[test] +fn test_delete_graph_and_print_match_gnu() { + let input = [b' ', b'A', b'!', b'\t', b'\n']; + new_ucmd!() + .args(&["-d", "[:graph:]"]) + .pipe_in(input) + .succeeds() + .stdout_is_bytes([b' ', b'\t', b'\n']); + + new_ucmd!() + .args(&["-d", "[:print:]"]) + .pipe_in(input) + .succeeds() + .stdout_is_bytes([b'\t', b'\n']); +} + +#[test] +fn test_delete_complement_graph_and_print_match_gnu() { + let input = [b' ', b'A', b'!', b'\t', b'\n']; + new_ucmd!() + .args(&["-d", "-c", "[:graph:]"]) + .pipe_in(input) + .succeeds() + .stdout_is_bytes([b'A', b'!']); + + new_ucmd!() + .args(&["-d", "-c", "[:print:]"]) + .pipe_in(input) + .succeeds() + .stdout_is_bytes([b' ', b'A', b'!']); +} + #[test] fn test_delete_afterwards_is_not_flag() { new_ucmd!() diff --git a/tests/fixtures/cksum/base64/blake3_single_file.expected b/tests/fixtures/cksum/base64/blake3_single_file.expected new file mode 100644 index 00000000000..cce3661ff3d --- /dev/null +++ b/tests/fixtures/cksum/base64/blake3_single_file.expected @@ -0,0 +1 @@ +BLAKE3-256 (lorem_ipsum.txt) = /c9QNtxcUOUQabtbQFNF7VC4iiTaZsb4shtGR3z2mdk= diff --git a/tests/fixtures/cksum/blake3_multiple_files.expected b/tests/fixtures/cksum/blake3_multiple_files.expected new file mode 100644 index 00000000000..f9541ed6b2c --- /dev/null +++ b/tests/fixtures/cksum/blake3_multiple_files.expected @@ -0,0 +1,2 @@ +BLAKE3-256 (lorem_ipsum.txt) = fdcf5036dc5c50e51069bb5b405345ed50b88a24da66c6f8b21b46477cf699d9 +BLAKE3-256 (alice_in_wonderland.txt) = 93c026644dc6b34a13a2b6705485d06f32d712c76ebe5353e935155edeeb49d5 diff --git a/tests/fixtures/cksum/blake3_single_file.expected b/tests/fixtures/cksum/blake3_single_file.expected new file mode 100644 index 00000000000..cfc017b947c --- /dev/null +++ b/tests/fixtures/cksum/blake3_single_file.expected @@ -0,0 +1 @@ +BLAKE3-256 (lorem_ipsum.txt) = fdcf5036dc5c50e51069bb5b405345ed50b88a24da66c6f8b21b46477cf699d9 diff --git a/tests/fixtures/cksum/blake3_stdin.expected b/tests/fixtures/cksum/blake3_stdin.expected new file mode 100644 index 00000000000..fbb69fbc1e9 --- /dev/null +++ b/tests/fixtures/cksum/blake3_stdin.expected @@ -0,0 +1 @@ +BLAKE3-256 (-) = fdcf5036dc5c50e51069bb5b405345ed50b88a24da66c6f8b21b46477cf699d9 diff --git a/tests/fixtures/cksum/raw/blake3_single_file.expected b/tests/fixtures/cksum/raw/blake3_single_file.expected new file mode 100644 index 00000000000..c9e56f00552 --- /dev/null +++ b/tests/fixtures/cksum/raw/blake3_single_file.expected @@ -0,0 +1 @@ +ýÏP6Ü\Påi»[@SEíP¸Š$ÚfÆø²FG|ö™Ù \ No newline at end of file diff --git a/tests/fixtures/cksum/untagged/blake3_multiple_files.expected b/tests/fixtures/cksum/untagged/blake3_multiple_files.expected new file mode 100644 index 00000000000..29379596a67 --- /dev/null +++ b/tests/fixtures/cksum/untagged/blake3_multiple_files.expected @@ -0,0 +1,2 @@ +fdcf5036dc5c50e51069bb5b405345ed50b88a24da66c6f8b21b46477cf699d9 lorem_ipsum.txt +93c026644dc6b34a13a2b6705485d06f32d712c76ebe5353e935155edeeb49d5 alice_in_wonderland.txt diff --git a/tests/fixtures/cksum/untagged/blake3_single_file.expected b/tests/fixtures/cksum/untagged/blake3_single_file.expected new file mode 100644 index 00000000000..1fafca356f4 --- /dev/null +++ b/tests/fixtures/cksum/untagged/blake3_single_file.expected @@ -0,0 +1 @@ +fdcf5036dc5c50e51069bb5b405345ed50b88a24da66c6f8b21b46477cf699d9 lorem_ipsum.txt diff --git a/tests/fixtures/cksum/untagged/blake3_stdin.expected b/tests/fixtures/cksum/untagged/blake3_stdin.expected new file mode 100644 index 00000000000..4da4171eec6 --- /dev/null +++ b/tests/fixtures/cksum/untagged/blake3_stdin.expected @@ -0,0 +1 @@ +fdcf5036dc5c50e51069bb5b405345ed50b88a24da66c6f8b21b46477cf699d9 - diff --git a/tests/test_util_name.rs b/tests/test_util_name.rs index 17b9dc36e5e..53c3ef55334 100644 --- a/tests/test_util_name.rs +++ b/tests/test_util_name.rs @@ -26,6 +26,20 @@ fn init() { eprintln!("Setting UUTESTS_BINARY_PATH={TESTS_BINARY}"); } +#[test] +#[cfg(all(feature = "env", any(target_os = "linux", target_os = "android")))] +fn binary_name_protection() { + let ts = TestScenario::new("env"); + let bin = ts.bin_path.clone(); + ts.ucmd() + .arg("-a") + .arg("hijacked") + .arg(&bin) + .arg("--version") + .succeeds() + .stdout_contains("coreutils"); +} + #[test] #[cfg(feature = "ls")] fn execution_phrase_double() { diff --git a/tests/uutests/LICENSE b/tests/uutests/LICENSE new file mode 120000 index 00000000000..30cff7403da --- /dev/null +++ b/tests/uutests/LICENSE @@ -0,0 +1 @@ +../../LICENSE \ No newline at end of file diff --git a/tests/uutests/src/lib/util.rs b/tests/uutests/src/lib/util.rs index 16258400710..6e5e9690d24 100644 --- a/tests/uutests/src/lib/util.rs +++ b/tests/uutests/src/lib/util.rs @@ -100,6 +100,19 @@ pub fn is_ci() -> bool { env::var("CI").is_ok_and(|s| s.eq_ignore_ascii_case("true")) } +/// Check if a locale is available on the system by verifying that +/// `locale charmap` returns `"UTF-8"` when `LC_ALL` is set to the given locale. +#[cfg(unix)] +pub fn is_locale_available(locale: &str) -> bool { + use std::process::Command; + Command::new("locale") + .env("LC_ALL", locale) + .arg("charmap") + .output() + .map(|o| String::from_utf8_lossy(&o.stdout).trim() == "UTF-8") + .unwrap_or(false) +} + /// Read a test scenario fixture, returning its bytes fn read_scenario_fixture>(tmpd: Option<&Rc>, file_rel_path: S) -> Vec { let tmpdir_path = tmpd.as_ref().unwrap().as_ref().path(); diff --git a/util/build-gnu.sh b/util/build-gnu.sh index 4a731f8aa29..6c5cb6145f6 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 +# spell-checker:ignore (paths) abmon deref discrim eacces getopt ginstall inacc infloop inotify reflink ; (misc) INT_OFLOW OFLOW # spell-checker:ignore baddecode submodules xstrtol distros ; (vars/env) SRCDIR vdir rcexp xpart dired OSTYPE ; (utils) greadlink gsed multihardlink texinfo CARGOFLAGS # spell-checker:ignore openat TOCTOU CFLAGS tmpfs gnproc @@ -95,11 +95,11 @@ else # Use MULTICALL=y for faster build make MULTICALL=y SKIP_UTILS=more for binary in $("${UU_BUILD_DIR}"/coreutils --list) - do [ -e "${UU_BUILD_DIR}/${binary}" ] || ln -vf "${UU_BUILD_DIR}/coreutils" "${UU_BUILD_DIR}/${binary}" + do ln -vf "${UU_BUILD_DIR}/coreutils" "${UU_BUILD_DIR}/${binary}" done ln -vf "${UU_BUILD_DIR}"/deps/libstdbuf.* -t "${UU_BUILD_DIR}" fi -[ -e "${UU_BUILD_DIR}/ginstall" ] || ln -vf "${UU_BUILD_DIR}/install" "${UU_BUILD_DIR}/ginstall" # The GNU tests use ginstall +ln -vf "${UU_BUILD_DIR}/install" "${UU_BUILD_DIR}/ginstall" # The GNU tests use ginstall ## cd "${path_GNU}" && echo "[ pwd:'${PWD}' ]" @@ -160,6 +160,10 @@ else touch gnu-built fi +# Keep getlimits available on PATH for GNU shell and Perl tests even when +# reusing an existing GNU build directory. +test -f src/getlimits && cp -f src/getlimits "${UU_BUILD_DIR}" + # Keep Makefile.in newer than the local.mk files we just modified, # and Makefile newer than Makefile.in, so make won't re-run # automake or config.status and undo our edits. diff --git a/util/fetch-gnu.sh b/util/fetch-gnu.sh index afd9ebfa88a..ae6de16e7fc 100755 --- a/util/fetch-gnu.sh +++ b/util/fetch-gnu.sh @@ -4,7 +4,12 @@ repo=https://github.com/coreutils/coreutils curl -L "${repo}/releases/download/v${ver}/coreutils-${ver}.tar.xz" | tar --strip-components=1 -xJf - # TODO stop backporting tests from master at GNU coreutils > $ver -# backport = () -# for f in ${backport[@]} -# do curl -L ${repo}/raw/refs/heads/master/tests/$f > tests/$f -# done + backport=( + misc/coreutils.sh # enable test + misc/yes.sh # zero-copy +) + for f in "${backport[@]}" + do curl -L ${repo}/raw/refs/heads/master/tests/$f > tests/$f + done +# adjust for getlimits > $ver +sed -i.b "s/\$ENOSPC/No space left on device/" tests/misc/yes.sh diff --git a/util/gnu-patches/series b/util/gnu-patches/series index f1a24d0f67e..973dce8b2bc 100644 --- a/util/gnu-patches/series +++ b/util/gnu-patches/series @@ -10,3 +10,4 @@ tests_sort_merge.pl.patch tests_du_move_dir_while_traversing.patch test_mkdir_restorecon.patch error_msg_uniq.diff +tests_numfmt.patch diff --git a/util/gnu-patches/tests_numfmt.patch b/util/gnu-patches/tests_numfmt.patch new file mode 100644 index 00000000000..ac7870e34a3 --- /dev/null +++ b/util/gnu-patches/tests_numfmt.patch @@ -0,0 +1,129 @@ +Index: gnu/tests/numfmt/numfmt.pl +=================================================================== +--- gnu.orig/tests/numfmt/numfmt.pl ++++ gnu/tests/numfmt/numfmt.pl +@@ -296,7 +296,7 @@ my @Tests = + + #Fields + ['field-1', '--field A', +- {ERR => "$prog: invalid field value 'A'\n$try"}, ++ {ERR => "$prog: range 'A' was invalid: failed to parse range\n"}, + {EXIT => '1'}], + ['field-2', '--field 2 --from=auto "Hello 40M World 90G"', + {OUT=>'Hello 40000000 World 90G'}], +@@ -379,30 +379,39 @@ my @Tests = + {OUT=>"1.0k 2.0k 3.0k 4.0k 5.0k"}], + + ['field-range-err-1', '--field -foo --to=si 10', +- {EXIT=>1}, {ERR=>"$prog: invalid field value 'foo'\n$try"}], ++ {EXIT=>1}, {ERR=>"$prog: range '-foo' was invalid: failed to parse range\n"}], + ['field-range-err-2', '--field --3 --to=si 10', +- {EXIT=>1}, {ERR=>"$prog: invalid field range\n$try"}], ++ {EXIT=>1}, {ERR=>"$prog: range '--3' was invalid: failed to parse range\n"}], + ['field-range-err-3', '--field 0 --to=si 10', +- {EXIT=>1}, {ERR=>"$prog: fields are numbered from 1\n$try"}], ++ {EXIT=>1}, {ERR=>"$prog: range '0' was invalid: fields and positions are numbered from 1\n"}], + ['field-range-err-4', '--field 3-2 --to=si 10', +- {EXIT=>1}, {ERR=>"$prog: invalid decreasing range\n$try"}], ++ {EXIT=>1}, {ERR=>"$prog: range '3-2' was invalid: high end of range less than low end\n"}], + ['field-range-err-6', '--field - --field 1- --to=si 10', +- {EXIT=>1}, {ERR=>"$prog: multiple field specifications\n"}], ++ {EXIT=>1}, ++ {ERR_SUBST=>"s/.*//msg"}, ++ {ERR=>""}], + ['field-range-err-7', '--field -1 --field 1- --to=si 10', +- {EXIT=>1}, {ERR=>"$prog: multiple field specifications\n"}], ++ {EXIT=>1}, ++ {ERR_SUBST=>"s/.*//msg"}, ++ {ERR=>""}], + ['field-range-err-8', '--field -1 --field 1,2,3 --to=si 10', +- {EXIT=>1}, {ERR=>"$prog: multiple field specifications\n"}], ++ {EXIT=>1}, ++ {ERR_SUBST=>"s/.*//msg"}, ++ {ERR=>""}], + ['field-range-err-9', '--field 1- --field 1,2,3 --to=si 10', +- {EXIT=>1}, {ERR=>"$prog: multiple field specifications\n"}], ++ {EXIT=>1}, ++ {ERR_SUBST=>"s/.*//msg"}, ++ {ERR=>""}], + ['field-range-err-10','--field 1,2,3 --field 1- --to=si 10', +- {EXIT=>1}, {ERR=>"$prog: multiple field specifications\n"}], ++ {EXIT=>1}, ++ {ERR_SUBST=>"s/.*//msg"}, ++ {ERR=>""}], + ['field-range-err-11','--field 1-2-3 --to=si 10', +- {EXIT=>1}, {ERR=>"$prog: invalid field range\n$try"}], ++ {EXIT=>1}, {ERR=>"$prog: range '1-2-3' was invalid: failed to parse range\n"}], + ['field-range-err-12','--field 0-1 --to=si 10', +- {EXIT=>1}, {ERR=>"$prog: fields are numbered from 1\n$try"}], ++ {EXIT=>1}, {ERR=>"$prog: range '0-1' was invalid: fields and positions are numbered from 1\n"}], + ['field-range-err-13','--field '.$limits->{UINTMAX_MAX}.',22 --to=si 10', +- {EXIT=>1}, {ERR=>"$prog: field number " . +- "'".$limits->{UINTMAX_MAX}."' is too large\n$try"}], ++ {EXIT=>1}, {ERR=>"$prog: range '".$limits->{UINTMAX_MAX}."' was invalid: byte/character offset is too large\n"}], + + # Auto-consume white-space, setup auto-padding + ['whitespace-1', '--to=si --field 2 "A 500 B"', {OUT=>"A 500 B"}], +@@ -706,41 +715,41 @@ my @Tests = + + # dev-debug messages - the actual messages don't matter + # just ensure the program works, and for code coverage testing. +- ['devdebug-1', '---debug --from=si 4.9K', {OUT=>"4900"}, ++ ['devdebug-1', '--debug --from=si 4.9K', {OUT=>"4900"}, + {ERR=>""}, + {ERR_SUBST=>"s/.*//msg"}], +- ['devdebug-2', '---debug 4900', {OUT=>"4900"}, ++ ['devdebug-2', '--debug 4900', {OUT=>"4900"}, + {ERR=>""}, + {ERR_SUBST=>"s/.*//msg"}], +- ['devdebug-3', '---debug --from=auto 4Mi', {OUT=>"4194304"}, ++ ['devdebug-3', '--debug --from=auto 4Mi', {OUT=>"4194304"}, + {ERR=>""}, + {ERR_SUBST=>"s/.*//msg"}], +- ['devdebug-4', '---debug --to=si 4000000', {OUT=>"4.0M"}, ++ ['devdebug-4', '--debug --to=si 4000000', {OUT=>"4.0M"}, + {ERR=>""}, + {ERR_SUBST=>"s/.*//msg"}], +- ['devdebug-5', '---debug --to=si --padding=5 4000000', {OUT=>" 4.0M"}, ++ ['devdebug-5', '--debug --to=si --padding=5 4000000', {OUT=>" 4.0M"}, + {ERR=>""}, + {ERR_SUBST=>"s/.*//msg"}], +- ['devdebug-6', '---debug --suffix=Foo 1234Foo', {OUT=>"1234Foo"}, ++ ['devdebug-6', '--debug --suffix=Foo 1234Foo', {OUT=>"1234Foo"}, + {ERR=>""}, + {ERR_SUBST=>"s/.*//msg"}], +- ['devdebug-7', '---debug --suffix=Foo 1234', {OUT=>"1234Foo"}, ++ ['devdebug-7', '--debug --suffix=Foo 1234', {OUT=>"1234Foo"}, + {ERR=>""}, + {ERR_SUBST=>"s/.*//msg"}], +- ['devdebug-9', '---debug --grouping 10000', {OUT=>"10000"}, ++ ['devdebug-9', '--debug --grouping 10000', {OUT=>"10000"}, + {ERR=>""}, + {ERR_SUBST=>"s/.*//msg"}], +- ['devdebug-10', '---debug --format %f 10000', {OUT=>"10000"}, ++ ['devdebug-10', '--debug --format %f 10000', {OUT=>"10000"}, + {ERR=>""}, + {ERR_SUBST=>"s/.*//msg"}], +- ['devdebug-11', '---debug --format "%\'-10f" 10000',{OUT=>"10000 "}, ++ ['devdebug-11', '--debug --format "%\'-10f" 10000',{OUT=>"10000 "}, + {ERR=>""}, + {ERR_SUBST=>"s/.*//msg"}], + + # Invalid parameters + ['help-1', '--foobar', +- {ERR=>"$prog: unrecognized option\n$try"}, +- {ERR_SUBST=>"s/option.*/option/; s/unknown/unrecognized/"}, ++ {ERR=>""}, ++ {ERR_SUBST=>"s/.*//msg"}, + {EXIT=>1}], + + ## Format string - check error detection +@@ -1097,7 +1106,7 @@ my @Limit_Tests = + {EXIT => 2}], + ); + # Restrict these tests to systems with LDBL_DIG == 18 +-(system "$prog ---debug 1 2>&1|grep 'MAX_UNSCALED_DIGITS: 18' > /dev/null") == 0 ++(system "$prog --debug 1 2>&1|grep 'MAX_UNSCALED_DIGITS: 18' > /dev/null") == 0 + and push @Tests, @Limit_Tests; + + my $lg = ' '; diff --git a/util/run-clippy.py b/util/run-clippy.py new file mode 100755 index 00000000000..5dcf805ba4d --- /dev/null +++ b/util/run-clippy.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: MIT +# spell-checker:ignore pcoreutils +"""Run cargo clippy with appropriate flags and emit GitHub Actions annotations.""" + +from __future__ import annotations + +import argparse +import json +import os +import re +import subprocess +import sys + + +def run_cmd( + cmd: list[str], + *, + check: bool = False, +) -> subprocess.CompletedProcess[str]: + """Run a command with UTF-8 encoding (avoids cp1252 issues on Windows).""" + env = {**os.environ, "PYTHONUTF8": "1"} + return subprocess.run( + cmd, + capture_output=True, + text=True, + encoding="utf-8", + errors="replace", + check=check, + env=env, + ) + + +def get_utility_list(features: str) -> list[str]: + """Get list of utilities from cargo metadata.""" + if features == "all": + cmd = ["cargo", "metadata", "--all-features", "--format-version", "1"] + else: + cmd = ["cargo", "metadata", "--features", features, "--format-version", "1"] + result = run_cmd(cmd, check=True) + metadata = json.loads(result.stdout) + # Find the coreutils root node and collect uu_ dependencies + utilities = [] + for node in metadata["resolve"]["nodes"]: + if re.search(r"coreutils[ @#]\d+\.\d+\.\d+", node["id"]): + for dep in node["deps"]: + # The pkg field contains the crate name (uu_), + # while name is the renamed dependency alias + pkg = dep["pkg"] + match = re.search(r"uu_(\w+)[@#]", pkg) + if match: + utilities.append(match.group(1)) + break + return sorted(utilities) + + +def build_clippy_command( + features: str, + *, + workspace: bool, + target: str | None, +) -> list[str]: + """Build the cargo clippy command line.""" + cmd = ["cargo", "clippy"] + + extra = [] + if features == "all": + extra.append("--all-features") + else: + extra.extend(["--features", features]) + + if workspace: + extra.append("--workspace") + + if target: + extra.extend(["--no-default-features", "--target", target]) + # For cross-compilation targets, just check -pcoreutils + # (show-utils.sh over-resolves due to default features) + extra.append("-pcoreutils") + else: + extra.extend(["--all-targets", "--tests", "--benches", "-pcoreutils"]) + utilities = get_utility_list(features) + extra.extend(f"-puu_{u}" for u in utilities) + + cmd.extend(extra) + cmd.extend(["--", "-D", "warnings"]) + return cmd + + +# Pattern to match clippy/rustc errors for GHA annotations +ERROR_PATTERN = re.compile( + r"^error:\s+(.*)\n\s+-->\s+(.*):(\d+):(\d+)", + re.MULTILINE, +) + + +def emit_annotations(output: str, fault_type: str) -> None: + """Emit GitHub Actions annotations from cargo clippy errors.""" + fault_prefix = fault_type.upper() + for m in ERROR_PATTERN.finditer(output): + message, file, line, col = m.groups() + print( + f"::{fault_type} file={file},line={line},col={col}" + f"::{fault_prefix}: `cargo clippy`: {message} (file:'{file}', line:{line})", + ) + + +def main() -> int: + """Run cargo clippy and emit GHA annotations on failure.""" + parser = argparse.ArgumentParser(description="Run cargo clippy for CI") + parser.add_argument("--features", required=True, help="Feature set to use") + parser.add_argument( + "--workspace", + action="store_true", + help="Include --workspace flag", + ) + parser.add_argument("--target", default=None, help="Cross-compilation target") + parser.add_argument( + "--fault-type", + default="warning", + choices=["warning", "error"], + help="GHA annotation type", + ) + parser.add_argument( + "--fail-on-fault", + action="store_true", + help="Exit with error code on clippy failures", + ) + args = parser.parse_args() + + cmd = build_clippy_command( + args.features, + workspace=args.workspace, + target=args.target, + ) + print(f"Running: {' '.join(cmd)}", file=sys.stderr) + + result = run_cmd(cmd) + output = result.stdout + result.stderr + + # Always print the full output + print(output) + + if result.returncode != 0: + emit_annotations(output, args.fault_type) + if args.fail_on_fault: + return 1 + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/util/run-gnu-tests-smack-ci.sh b/util/run-gnu-tests-smack-ci.sh index e37e29ca3fa..867a15dad4a 100755 --- a/util/run-gnu-tests-smack-ci.sh +++ b/util/run-gnu-tests-smack-ci.sh @@ -17,15 +17,14 @@ mkdir -p "$QEMU_DIR"/{rootfs/{bin,lib64,proc,sys,dev,tmp,etc,gnu},kernel} # Copy Ubuntu kernel (runner's kernel does not work) sudo apt-get update || : -sudo apt-get install -y linux-image-generic +sudo apt-get install -y --no-install-recommends linux-image-generic sudo install -Dvm644 "$(ls -1 /boot/vmlinuz-*-generic | head -n 1)" "$QEMU_DIR/kernel/vmlinuz" # Setup busybox -BUSYBOX=/tmp/busybox -[ -f "$BUSYBOX" ] || curl -sL -o "$BUSYBOX" https://busybox.net/downloads/binaries/1.35.0-x86_64-linux-musl/busybox -chmod +x "$BUSYBOX" -cp "$BUSYBOX" "$QEMU_DIR/rootfs/bin/" -(cd "$QEMU_DIR/rootfs/bin" && "$BUSYBOX" --list | xargs -I{} ln -sf busybox {} 2>/dev/null) +curl -L -o b.tar.gz https://dl-cdn.alpinelinux.org/alpine/latest-stable/main/x86_64/busybox-static-1.37.0-r30.apk +tar -xf b.tar.gz +install -Dvm755 bin/busybox.static "$QEMU_DIR/rootfs/bin/busybox" +(cd "$QEMU_DIR/rootfs/bin" && ./busybox --list | xargs -I{} ln -sf busybox {} 2>/dev/null) # Copy required libraries for lib in ld-linux-x86-64.so.2 libc.so.6 libm.so.6 libgcc_s.so.1 libpthread.so.0 libdl.so.2 librt.so.1; do @@ -104,7 +103,7 @@ for TEST_PATH in $QEMU_TESTS; do # Hardlink utilities for SMACK/ROOTFS tests for U in $("$REPO_DIR/target/${PROFILE}/coreutils" --list); do - ln -vf "$REPO_DIR/target/${PROFILE}/coreutils" "$WORK/bin/$U" + ln -f "$REPO_DIR/target/${PROFILE}/coreutils" "$WORK/bin/$U" done # Set test script path and user diff --git a/util/update-version.sh b/util/update-version.sh index 1290cc213b1..d80467e22b7 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.6.0" -TO="0.7.0" +FROM="0.7.0" +TO="0.8.0" PROGS=$(ls -1d src/uu/*/Cargo.toml src/uu/stdbuf/src/libstdbuf/Cargo.toml src/uucore/Cargo.toml Cargo.toml fuzz/uufuzz/Cargo.toml src/uu/stdbuf/Cargo.toml) @@ -48,3 +48,7 @@ sed -i -e "s|uucore = { version=\">=$FROM\",|uucore = { version=\">=$TO\",|" $PR # Update crates using uucore_procs #shellcheck disable=SC2086 sed -i -e "s|uucore_procs = { version=\">=$FROM\",|uucore_procs = { version=\">=$TO\",|" $PROGS + +# Update Cargo.lock files +cargo update --workspace +cargo update --workspace --manifest-path fuzz/Cargo.toml